env.info( '*** MOOSE GITHUB Commit Hash ID: 2018-11-14T18:03:42.0000000Z-ec53b89c695b999dec7775be5f7f7386368c052c ***' )
env.info( '*** MOOSE STATIC INCLUDE START *** ' )

--- Various routines
-- @module routines
-- @image MOOSE.JPG

env.setErrorMessageBoxEnabled(false)

--- Extract of MIST functions.
-- @author Grimes

routines = {}


-- don't change these
routines.majorVersion = 3
routines.minorVersion = 3
routines.build = 22

-----------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------
-- Utils- conversion, Lua utils, etc.
routines.utils = {}

--from http://lua-users.org/wiki/CopyTable
routines.utils.deepCopy = function(object)
	local lookup_table = {}
	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


-- porting in Slmod's serialize_slmod2
routines.utils.oneLineSerialize = function(tbl)  -- serialization of a table all on a single line, no comments, made to replace old get_table_string function

	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] = routines.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] = routines.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] = "function " .. tostring(ind)
				--	tbl_str[#tbl_str + 1] = ','   --I think this is right, I just added it
				else
--					env.info('unable to serialize value type ' .. routines.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
		  if type(tbl) == 'string' then
		    return tbl
		  else
			return tostring(tbl)
			end
		end
	end
	
	local objectreturn = _Serialize(tbl)
	return objectreturn
end

--porting in Slmod's "safestring" basic serialize
routines.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) == 'table') or (type(s) == 'userdata') ) then
			return tostring(s)
		elseif type(s) == 'string' then
			s = string.format('%s', s:gsub( "%%", "%%%%" ) )
			return s
		end
	end
end


routines.utils.toDegree = function(angle)
	return angle*180/math.pi
end

routines.utils.toRadian = function(angle)
	return angle*math.pi/180
end

routines.utils.metersToNM = function(meters)
	return meters/1852
end

routines.utils.metersToFeet = function(meters)
	return meters/0.3048
end

routines.utils.NMToMeters = function(NM)
	return NM*1852
end

routines.utils.feetToMeters = function(feet)
	return feet*0.3048
end

routines.utils.mpsToKnots = function(mps)
	return mps*3600/1852
end

routines.utils.mpsToKmph = function(mps)
	return mps*3.6
end

routines.utils.knotsToMps = function(knots)
	return knots*1852/3600
end

routines.utils.kmphToMps = function(kmph)
	return kmph/3.6
end

function routines.utils.makeVec2(Vec3)
	if Vec3.z then
		return {x = Vec3.x, y = Vec3.z}
	else
		return {x = Vec3.x, y = Vec3.y}  -- it was actually already vec2.
	end
end

function routines.utils.makeVec3(Vec2, y)
	if not Vec2.z then
		if not y then
			y = 0
		end
		return {x = Vec2.x, y = y, z = Vec2.y}
	else
		return {x = Vec2.x, y = Vec2.y, z = Vec2.z}  -- it was already Vec3, actually.
	end
end

function routines.utils.makeVec3GL(Vec2, offset)
	local adj = offset or 0

	if not Vec2.z then
		return {x = Vec2.x, y = (land.getHeight(Vec2) + adj), z = Vec2.y}
	else
		return {x = Vec2.x, y = (land.getHeight({x = Vec2.x, y = Vec2.z}) + adj), z = Vec2.z}
	end
end

routines.utils.zoneToVec3 = function(zone)
	local new = {}
	if type(zone) == 'table' and zone.point then
		new.x = zone.point.x
		new.y = zone.point.y
		new.z = zone.point.z
		return new
	elseif type(zone) == 'string' then
		zone = trigger.misc.getZone(zone)
		if zone then
			new.x = zone.point.x
			new.y = zone.point.y
			new.z = zone.point.z
			return new
		end
	end
end

-- gets heading-error corrected direction from point along vector vec.
function routines.utils.getDir(vec, point)
	local dir = math.atan2(vec.z, vec.x)
	dir = dir + routines.getNorthCorrection(point)
	if dir < 0 then
		dir = dir + 2*math.pi  -- put dir in range of 0 to 2*pi
	end
	return dir
end

-- gets distance in meters between two points (2 dimensional)
function routines.utils.get2DDist(point1, point2)
	point1 = routines.utils.makeVec3(point1)
	point2 = routines.utils.makeVec3(point2)
	return routines.vec.mag({x = point1.x - point2.x, y = 0, z = point1.z - point2.z})
end

-- gets distance in meters between two points (3 dimensional)
function routines.utils.get3DDist(point1, point2)
	return routines.vec.mag({x = point1.x - point2.x, y = point1.y - point2.y, z = point1.z - point2.z})
end





--3D Vector manipulation
routines.vec = {}

routines.vec.add = function(vec1, vec2)
	return {x = vec1.x + vec2.x, y = vec1.y + vec2.y, z = vec1.z + vec2.z}
end

routines.vec.sub = function(vec1, vec2)
	return {x = vec1.x - vec2.x, y = vec1.y - vec2.y, z = vec1.z - vec2.z}
end

routines.vec.scalarMult = function(vec, mult)
	return {x = vec.x*mult, y = vec.y*mult, z = vec.z*mult}
end

routines.vec.scalar_mult = routines.vec.scalarMult

routines.vec.dp = function(vec1, vec2)
	return vec1.x*vec2.x + vec1.y*vec2.y + vec1.z*vec2.z
end

routines.vec.cp = function(vec1, vec2)
	return { x = vec1.y*vec2.z - vec1.z*vec2.y, y = vec1.z*vec2.x - vec1.x*vec2.z, z = vec1.x*vec2.y - vec1.y*vec2.x}
end

routines.vec.mag = function(vec)
	return (vec.x^2 + vec.y^2 + vec.z^2)^0.5
end

routines.vec.getUnitVec = function(vec)
	local mag = routines.vec.mag(vec)
	return { x = vec.x/mag, y = vec.y/mag, z = vec.z/mag }
end

routines.vec.rotateVec2 = function(vec2, theta)
	return { x = vec2.x*math.cos(theta) - vec2.y*math.sin(theta), y = vec2.x*math.sin(theta) + vec2.y*math.cos(theta)}
end
---------------------------------------------------------------------------------------------------------------------------




-- acc- the accuracy of each easting/northing.  0, 1, 2, 3, 4, or 5.
routines.tostringMGRS = function(MGRS, acc)
	if acc == 0 then
		return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph
	else
		return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Easting/(10^(5-acc)), 0))
		       .. ' ' .. string.format('%0' .. acc .. 'd', routines.utils.round(MGRS.Northing/(10^(5-acc)), 0))
	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.
]]
routines.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 = routines.utils.round((oldLatMin - latMin)*60, acc)

		local oldLonMin = lonMin
		lonMin = math.floor(lonMin)
		local lonSec = routines.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
		if acc <= 0 then  -- no decimal place.
			secFrmtStr = '%02d'
		else
			local width = 3 + acc  -- 01.310 - that's a width of 6, for example.
			secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
		end

		return string.format('%02d', latDeg) .. ' ' .. string.format('%02d', latMin) .. '\' ' .. string.format(secFrmtStr, latSec) .. '"' .. latHemi .. '   '
		       .. string.format('%02d', lonDeg) .. ' ' .. string.format('%02d', lonMin) .. '\' ' .. string.format(secFrmtStr, lonSec) .. '"' .. lonHemi

	else  -- degrees, decimal minutes.
		latMin = routines.utils.round(latMin, acc)
		lonMin = routines.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

		return string.format('%02d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. '   '
	   .. string.format('%02d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi

	end
end

--[[ required: az - radian
     required: dist - meters
	 optional: alt - meters (set to false or nil if you don't want to use it).
	 optional: metric - set true to get dist and alt in km and m.
	 precision will always be nearest degree and NM or km.]]
routines.tostringBR = function(az, dist, alt, metric)
	az = routines.utils.round(routines.utils.toDegree(az), 0)

	if metric then
		dist = routines.utils.round(dist/1000, 2)
	else
		dist = routines.utils.round(routines.utils.metersToNM(dist), 2)
	end

	local s = string.format('%03d', az) .. ' for ' .. dist

	if alt then
		if metric then
			s = s .. ' at ' .. routines.utils.round(alt, 0)
		else
			s = s .. ' at ' .. routines.utils.round(routines.utils.metersToFeet(alt), 0)
		end
	end
	return s
end

routines.getNorthCorrection = function(point)  --gets the correction needed for true north
	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


do
	local idNum = 0

	--Simplified event handler
	routines.addEventHandler = function(f) --id is optional!
		local handler = {}
		idNum = idNum + 1
		handler.id = idNum
		handler.f = f
		handler.onEvent = function(self, event)
			self.f(event)
		end
		world.addEventHandler(handler)
	end

	routines.removeEventHandler = function(id)
		for key, handler in pairs(world.eventHandlers) do
			if handler.id and handler.id == id then
				world.eventHandlers[key] = nil
				return true
			end
		end
		return false
	end
end

-- need to return a Vec3 or Vec2?
function routines.getRandPointInCircle(point, radius, innerRadius)
	local theta = 2*math.pi*math.random()
	local rad = math.random() + math.random()
	if rad > 1 then
		rad = 2 - rad
	end

	local radMult
	if innerRadius and innerRadius <= radius then
		radMult = (radius - innerRadius)*rad + innerRadius
	else
		radMult = radius*rad
	end

	if not point.z then --might as well work with vec2/3
		point.z = point.y
	end

	local rndCoord
	if radius > 0 then
		rndCoord = {x = math.cos(theta)*radMult + point.x, y = math.sin(theta)*radMult + point.z}
	else
		rndCoord = {x = point.x, y = point.z}
	end
	return rndCoord
end

routines.goRoute = function(group, path)
	local misTask = {
		id = 'Mission',
		params = {
			route = {
				points = routines.utils.deepCopy(path),
			},
		},
	}
	if type(group) == 'string' then
		group = Group.getByName(group)
	end
	local groupCon = group:getController()
	if groupCon then
		groupCon:setTask(misTask)
		return true
	end

	Controller.setTask(groupCon, misTask)
	return false
end


-- Useful atomic functions from mist, ported.

routines.ground = {}
routines.fixedWing = {}
routines.heli = {}

routines.ground.buildWP = function(point, overRideForm, overRideSpeed)

	local wp = {}
	wp.x = point.x

	if point.z then
		wp.y = point.z
	else
		wp.y = point.y
	end
	local form, speed

	if point.speed and not overRideSpeed then
		wp.speed = point.speed
	elseif type(overRideSpeed) == 'number' then
		wp.speed = overRideSpeed
	else
		wp.speed = routines.utils.kmphToMps(20)
	end

	if point.form and not overRideForm then
		form = point.form
	else
		form = overRideForm
	end

	if not form then
		wp.action = 'Cone'
	else
		form = string.lower(form)
		if form == 'off_road' or form == 'off road' then
			wp.action = 'Off Road'
		elseif form == 'on_road' or form == 'on road' then
			wp.action = 'On Road'
		elseif form == 'rank' or form == 'line_abrest' or form == 'line abrest' or form == 'lineabrest'then
			wp.action = 'Rank'
		elseif form == 'cone' then
			wp.action = 'Cone'
		elseif form == 'diamond' then
			wp.action = 'Diamond'
		elseif form == 'vee' then
			wp.action = 'Vee'
		elseif form == 'echelon_left' or form == 'echelon left' or form == 'echelonl' then
			wp.action = 'EchelonL'
		elseif form == 'echelon_right' or form == 'echelon right' or form == 'echelonr' then
			wp.action = 'EchelonR'
		else
			wp.action = 'Cone' -- if nothing matched
		end
	end

	wp.type = 'Turning Point'

	return wp

end

routines.fixedWing.buildWP = function(point, WPtype, speed, alt, altType)

	local wp = {}
	wp.x = point.x

	if point.z then
		wp.y = point.z
	else
		wp.y = point.y
	end

	if alt and type(alt) == 'number' then
		wp.alt = alt
	else
		wp.alt = 2000
	end

	if altType then
		altType = string.lower(altType)
		if altType == 'radio' or 'agl' then
			wp.alt_type = 'RADIO'
		elseif altType == 'baro' or 'asl' then
			wp.alt_type = 'BARO'
		end
	else
		wp.alt_type = 'RADIO'
	end

	if point.speed then
		speed = point.speed
	end

	if point.type then
		WPtype = point.type
	end

	if not speed then
		wp.speed = routines.utils.kmphToMps(500)
	else
		wp.speed = speed
	end

	if not WPtype then
		wp.action =  'Turning Point'
	else
		WPtype = string.lower(WPtype)
		if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then
			wp.action =  'Fly Over Point'
		elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then
			wp.action =  'Turning Point'
		else
			wp.action = 'Turning Point'
		end
	end

	wp.type = 'Turning Point'
	return wp
end

routines.heli.buildWP = function(point, WPtype, speed, alt, altType)

	local wp = {}
	wp.x = point.x

	if point.z then
		wp.y = point.z
	else
		wp.y = point.y
	end

	if alt and type(alt) == 'number' then
		wp.alt = alt
	else
		wp.alt = 500
	end

	if altType then
		altType = string.lower(altType)
		if altType == 'radio' or 'agl' then
			wp.alt_type = 'RADIO'
		elseif altType == 'baro' or 'asl' then
			wp.alt_type = 'BARO'
		end
	else
		wp.alt_type = 'RADIO'
	end

	if point.speed then
		speed = point.speed
	end

	if point.type then
		WPtype = point.type
	end

	if not speed then
		wp.speed = routines.utils.kmphToMps(200)
	else
		wp.speed = speed
	end

	if not WPtype then
		wp.action =  'Turning Point'
	else
		WPtype = string.lower(WPtype)
		if WPtype == 'flyover' or WPtype == 'fly over' or WPtype == 'fly_over' then
			wp.action =  'Fly Over Point'
		elseif WPtype == 'turningpoint' or WPtype == 'turning point' or WPtype == 'turning_point' then
			wp.action = 'Turning Point'
		else
			wp.action =  'Turning Point'
		end
	end

	wp.type = 'Turning Point'
	return wp
end

routines.groupToRandomPoint = function(vars)
	local group = vars.group --Required
	local point = vars.point --required
	local radius = vars.radius or 0
	local innerRadius = vars.innerRadius
	local form = vars.form or 'Cone'
	local heading = vars.heading or math.random()*2*math.pi
	local headingDegrees = vars.headingDegrees
	local speed = vars.speed or routines.utils.kmphToMps(20)


	local useRoads
	if not vars.disableRoads then
		useRoads = true
	else
		useRoads = false
	end

	local path = {}

	if headingDegrees then
		heading = headingDegrees*math.pi/180
	end

	if heading >= 2*math.pi then
		heading = heading - 2*math.pi
	end

	local rndCoord = routines.getRandPointInCircle(point, radius, innerRadius)

	local offset = {}
	local posStart = routines.getLeadPos(group)

	offset.x = routines.utils.round(math.sin(heading - (math.pi/2)) * 50 + rndCoord.x, 3)
	offset.z = routines.utils.round(math.cos(heading + (math.pi/2)) * 50 + rndCoord.y, 3)
	path[#path + 1] = routines.ground.buildWP(posStart, form, speed)


	if useRoads == true and ((point.x - posStart.x)^2 + (point.z - posStart.z)^2)^0.5 > radius * 1.3 then
		path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 11, ['z'] = posStart.z + 11}, 'off_road', speed)
		path[#path + 1] = routines.ground.buildWP(posStart, 'on_road', speed)
		path[#path + 1] = routines.ground.buildWP(offset, 'on_road', speed)
	else
		path[#path + 1] = routines.ground.buildWP({['x'] = posStart.x + 25, ['z'] = posStart.z + 25}, form, speed)
	end

	path[#path + 1] = routines.ground.buildWP(offset, form, speed)
	path[#path + 1] = routines.ground.buildWP(rndCoord, form, speed)

	routines.goRoute(group, path)

	return
end

routines.groupRandomDistSelf = function(gpData, dist, form, heading, speed)
	local pos = routines.getLeadPos(gpData)
	local fakeZone = {}
	fakeZone.radius = dist or math.random(300, 1000)
	fakeZone.point = {x = pos.x, y, pos.y, z = pos.z}
	routines.groupToRandomZone(gpData, fakeZone, form, heading, speed)

	return
end

routines.groupToRandomZone = function(gpData, zone, form, heading, speed)
	if type(gpData) == 'string' then
		gpData = Group.getByName(gpData)
	end

	if type(zone) == 'string' then
		zone = trigger.misc.getZone(zone)
	elseif type(zone) == 'table' and not zone.radius then
		zone = trigger.misc.getZone(zone[math.random(1, #zone)])
	end

	if speed then
		speed = routines.utils.kmphToMps(speed)
	end

	local vars = {}
	vars.group = gpData
	vars.radius = zone.radius
	vars.form = form
	vars.headingDegrees = heading
	vars.speed = speed
	vars.point = routines.utils.zoneToVec3(zone)

	routines.groupToRandomPoint(vars)

	return
end

routines.isTerrainValid = function(coord, terrainTypes) -- vec2/3 and enum or table of acceptable terrain types
	if coord.z then
		coord.y = coord.z
	end
	local typeConverted = {}

	if type(terrainTypes) == 'string' then -- if its a string it does this check
		for constId, constData in pairs(land.SurfaceType) do
			if string.lower(constId) == string.lower(terrainTypes) or string.lower(constData) == string.lower(terrainTypes) then
				table.insert(typeConverted, constId)
			end
		end
	elseif type(terrainTypes) == 'table' then -- if its a table it does this check
		for typeId, typeData in pairs(terrainTypes) do
			for constId, constData in pairs(land.SurfaceType) do
				if string.lower(constId) == string.lower(typeData) or string.lower(constData) == string.lower(typeId) then
					table.insert(typeConverted, constId)
				end
			end
		end
	end
	for validIndex, validData in pairs(typeConverted) do
		if land.getSurfaceType(coord) == land.SurfaceType[validData] then
			return true
		end
	end
	return false
end

routines.groupToPoint = function(gpData, point, form, heading, speed, useRoads)
	if type(point) == 'string' then
		point = trigger.misc.getZone(point)
	end
	if speed then
		speed = routines.utils.kmphToMps(speed)
	end

	local vars = {}
	vars.group = gpData
	vars.form = form
	vars.headingDegrees = heading
	vars.speed = speed
	vars.disableRoads = useRoads
	vars.point = routines.utils.zoneToVec3(point)
	routines.groupToRandomPoint(vars)

	return
end


routines.getLeadPos = function(group)
	if type(group) == 'string' then -- group name
		group = Group.getByName(group)
	end

	local units = group:getUnits()

	local leader = units[1]
	if not leader then  -- SHOULD be good, but if there is a bug, this code future-proofs it then.
		local lowestInd = math.huge
		for ind, unit in pairs(units) do
			if ind < lowestInd then
				lowestInd = ind
				leader = unit
			end
		end
	end
	if leader and Unit.isExist(leader) then  -- maybe a little too paranoid now...
		return leader:getPosition().p
	end
end

--[[ vars for routines.getMGRSString:
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer between 0 and 5, inclusive
]]
routines.getMGRSString = function(vars)
	local units = vars.units
	local acc = vars.acc or 5
	local avgPos = routines.getAvgPos(units)
	if avgPos then
		return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(avgPos)), acc)
	end
end

--[[ vars for routines.getLLString
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer, number of numbers after decimal place
vars.DMS - if true, output in degrees, minutes, seconds.  Otherwise, output in degrees, minutes.


]]
routines.getLLString = function(vars)
	local units = vars.units
	local acc = vars.acc or 3
	local DMS = vars.DMS
	local avgPos = routines.getAvgPos(units)
	if avgPos then
		local lat, lon = coord.LOtoLL(avgPos)
		return routines.tostringLL(lat, lon, acc, DMS)
	end
end

--[[
vars.zone - table of a zone name.
vars.ref -  vec3 ref point, maybe overload for vec2 as well?
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
]]
routines.getBRStringZone = function(vars)
	local zone = trigger.misc.getZone( vars.zone )
	local ref = routines.utils.makeVec3(vars.ref, 0)  -- turn it into Vec3 if it is not already.
	local alt = vars.alt
	local metric = vars.metric
	if zone then
		local vec = {x = zone.point.x - ref.x, y = zone.point.y - ref.y, z = zone.point.z - ref.z}
		local dir = routines.utils.getDir(vec, ref)
		local dist = routines.utils.get2DDist(zone.point, ref)
		if alt then
			alt = zone.y
		end
		return routines.tostringBR(dir, dist, alt, metric)
	else
		env.info( 'routines.getBRStringZone: error: zone is nil' )
	end
end

--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref -  vec3 ref point, maybe overload for vec2 as well?
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
]]
routines.getBRString = function(vars)
	local units = vars.units
	local ref = routines.utils.makeVec3(vars.ref, 0)  -- turn it into Vec3 if it is not already.
	local alt = vars.alt
	local metric = vars.metric
	local avgPos = routines.getAvgPos(units)
	if avgPos then
		local vec = {x = avgPos.x - ref.x, y = avgPos.y - ref.y, z = avgPos.z - ref.z}
		local dir = routines.utils.getDir(vec, ref)
		local dist = routines.utils.get2DDist(avgPos, ref)
		if alt then
			alt = avgPos.y
		end
		return routines.tostringBR(dir, dist, alt, metric)
	end
end


-- Returns the Vec3 coordinates of the average position of the concentration of units most in the heading direction.
--[[ vars for routines.getLeadingPos:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
]]
routines.getLeadingPos = function(vars)
	local units = vars.units
	local heading = vars.heading
	local radius = vars.radius
	if vars.headingDegrees then
		heading = routines.utils.toRadian(vars.headingDegrees)
	end

	local unitPosTbl = {}
	for i = 1, #units do
		local unit = Unit.getByName(units[i])
		if unit and unit:isExist() then
			unitPosTbl[#unitPosTbl + 1] = unit:getPosition().p
		end
	end
	if #unitPosTbl > 0 then  -- one more more units found.
		-- first, find the unit most in the heading direction
		local maxPos = -math.huge

		local maxPosInd  -- maxPos - the furthest in direction defined by heading; maxPosInd =
		for i = 1, #unitPosTbl do
			local rotatedVec2 = routines.vec.rotateVec2(routines.utils.makeVec2(unitPosTbl[i]), heading)
			if (not maxPos) or maxPos < rotatedVec2.x then
				maxPos = rotatedVec2.x
				maxPosInd = i
			end
		end

		--now, get all the units around this unit...
		local avgPos
		if radius then
			local maxUnitPos = unitPosTbl[maxPosInd]
			local avgx, avgy, avgz, totNum = 0, 0, 0, 0
			for i = 1, #unitPosTbl do
				if routines.utils.get2DDist(maxUnitPos, unitPosTbl[i]) <= radius then
					avgx = avgx + unitPosTbl[i].x
					avgy = avgy + unitPosTbl[i].y
					avgz = avgz + unitPosTbl[i].z
					totNum = totNum + 1
				end
			end
			avgPos = { x = avgx/totNum, y = avgy/totNum, z = avgz/totNum}
		else
			avgPos = unitPosTbl[maxPosInd]
		end

		return avgPos
	end
end


--[[ vars for routines.getLeadingMGRSString:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.acc - number, 0 to 5.
]]
routines.getLeadingMGRSString = function(vars)
	local pos = routines.getLeadingPos(vars)
	if pos then
		local acc = vars.acc or 5
		return routines.tostringMGRS(coord.LLtoMGRS(coord.LOtoLL(pos)), acc)
	end
end

--[[ vars for routines.getLeadingLLString:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.acc - number of digits after decimal point (can be negative)
vars.DMS -  boolean, true if you want DMS.
]]
routines.getLeadingLLString = function(vars)
	local pos = routines.getLeadingPos(vars)
	if pos then
		local acc = vars.acc or 3
		local DMS = vars.DMS
		local lat, lon = coord.LOtoLL(pos)
		return routines.tostringLL(lat, lon, acc, DMS)
	end
end



--[[ vars for routines.getLeadingBRString:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees
vars.metric - boolean, if true, use km instead of NM.
vars.alt - boolean, if true, include altitude.
vars.ref - vec3/vec2 reference point.
]]
routines.getLeadingBRString = function(vars)
	local pos = routines.getLeadingPos(vars)
	if pos then
		local ref = vars.ref
		local alt = vars.alt
		local metric = vars.metric

		local vec = {x = pos.x - ref.x, y = pos.y - ref.y, z = pos.z - ref.z}
		local dir = routines.utils.getDir(vec, ref)
		local dist = routines.utils.get2DDist(pos, ref)
		if alt then
			alt = pos.y
		end
		return routines.tostringBR(dir, dist, alt, metric)
	end
end

--[[ vars for routines.message.add
	vars.text = 'Hello World'
	vars.displayTime = 20
	vars.msgFor = {coa = {'red'}, countries = {'Ukraine', 'Georgia'}, unitTypes = {'A-10C'}}

]]

--[[ vars for routines.msgMGRS
vars.units - table of unit names (NOT unitNameTable- maybe this should change).
vars.acc - integer between 0 and 5, inclusive
vars.text - text in the message
vars.displayTime - self explanatory
vars.msgFor - scope
]]
routines.msgMGRS = function(vars)
	local units = vars.units
	local acc = vars.acc
	local text = vars.text
	local displayTime = vars.displayTime
	local msgFor = vars.msgFor

	local s = routines.getMGRSString{units = units, acc = acc}
	local newText
	if string.find(text, '%%s') then  -- look for %s
		newText = string.format(text, s)  -- insert the coordinates into the message
	else  -- else, just append to the end.
		newText = text .. s
	end

	routines.message.add{
		text = newText,
		displayTime = displayTime,
		msgFor = msgFor
	}
end

--[[ vars for routines.msgLL
vars.units - table of unit names (NOT unitNameTable- maybe this should change) (Yes).
vars.acc - integer, number of numbers after decimal place
vars.DMS - if true, output in degrees, minutes, seconds.  Otherwise, output in degrees, minutes.
vars.text - text in the message
vars.displayTime - self explanatory
vars.msgFor - scope
]]
routines.msgLL = function(vars)
	local units = vars.units  -- technically, I don't really need to do this, but it helps readability.
	local acc = vars.acc
	local DMS = vars.DMS
	local text = vars.text
	local displayTime = vars.displayTime
	local msgFor = vars.msgFor

	local s = routines.getLLString{units = units, acc = acc, DMS = DMS}
	local newText
	if string.find(text, '%%s') then  -- look for %s
		newText = string.format(text, s)  -- insert the coordinates into the message
	else  -- else, just append to the end.
		newText = text .. s
	end

	routines.message.add{
		text = newText,
		displayTime = displayTime,
		msgFor = msgFor
	}

end


--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref -  vec3 ref point, maybe overload for vec2 as well?
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
routines.msgBR = function(vars)
	local units = vars.units  -- technically, I don't really need to do this, but it helps readability.
	local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString
	local alt = vars.alt
	local metric = vars.metric
	local text = vars.text
	local displayTime = vars.displayTime
	local msgFor = vars.msgFor

	local s = routines.getBRString{units = units, ref = ref, alt = alt, metric = metric}
	local newText
	if string.find(text, '%%s') then  -- look for %s
		newText = string.format(text, s)  -- insert the coordinates into the message
	else  -- else, just append to the end.
		newText = text .. s
	end

	routines.message.add{
		text = newText,
		displayTime = displayTime,
		msgFor = msgFor
	}

end


--------------------------------------------------------------------------------------------
-- basically, just sub-types of routines.msgBR... saves folks the work of getting the ref point.
--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref -  string red, blue
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
routines.msgBullseye = function(vars)
	if string.lower(vars.ref) == 'red' then
		vars.ref = routines.DBs.missionData.bullseye.red
		routines.msgBR(vars)
	elseif string.lower(vars.ref) == 'blue' then
		vars.ref = routines.DBs.missionData.bullseye.blue
		routines.msgBR(vars)
	end
end

--[[
vars.units- table of unit names (NOT unitNameTable- maybe this should change).
vars.ref -  unit name of reference point
vars.alt - boolean, if used, includes altitude in string
vars.metric - boolean, gives distance in km instead of NM.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]

routines.msgBRA = function(vars)
	if Unit.getByName(vars.ref) then
		vars.ref = Unit.getByName(vars.ref):getPosition().p
		if not vars.alt then
			vars.alt = true
		end
		routines.msgBR(vars)
	end
end
--------------------------------------------------------------------------------------------

--[[ vars for routines.msgLeadingMGRS:
vars.units - table of unit names
vars.heading - direction
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees (optional)
vars.acc - number, 0 to 5.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
routines.msgLeadingMGRS = function(vars)
	local units = vars.units  -- technically, I don't really need to do this, but it helps readability.
	local heading = vars.heading
	local radius = vars.radius
	local headingDegrees = vars.headingDegrees
	local acc = vars.acc
	local text = vars.text
	local displayTime = vars.displayTime
	local msgFor = vars.msgFor

	local s = routines.getLeadingMGRSString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc}
	local newText
	if string.find(text, '%%s') then  -- look for %s
		newText = string.format(text, s)  -- insert the coordinates into the message
	else  -- else, just append to the end.
		newText = text .. s
	end

	routines.message.add{
		text = newText,
		displayTime = displayTime,
		msgFor = msgFor
	}


end
--[[ vars for routines.msgLeadingLL:
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees (optional)
vars.acc - number of digits after decimal point (can be negative)
vars.DMS -  boolean, true if you want DMS. (optional)
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
routines.msgLeadingLL = function(vars)
	local units = vars.units  -- technically, I don't really need to do this, but it helps readability.
	local heading = vars.heading
	local radius = vars.radius
	local headingDegrees = vars.headingDegrees
	local acc = vars.acc
	local DMS = vars.DMS
	local text = vars.text
	local displayTime = vars.displayTime
	local msgFor = vars.msgFor

	local s = routines.getLeadingLLString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, acc = acc, DMS = DMS}
	local newText
	if string.find(text, '%%s') then  -- look for %s
		newText = string.format(text, s)  -- insert the coordinates into the message
	else  -- else, just append to the end.
		newText = text .. s
	end

	routines.message.add{
		text = newText,
		displayTime = displayTime,
		msgFor = msgFor
	}

end

--[[
vars.units - table of unit names
vars.heading - direction, number
vars.radius - number
vars.headingDegrees - boolean, switches heading to degrees  (optional)
vars.metric - boolean, if true, use km instead of NM. (optional)
vars.alt - boolean, if true, include altitude. (optional)
vars.ref - vec3/vec2 reference point.
vars.text - text of the message
vars.displayTime
vars.msgFor - scope
]]
routines.msgLeadingBR = function(vars)
	local units = vars.units  -- technically, I don't really need to do this, but it helps readability.
	local heading = vars.heading
	local radius = vars.radius
	local headingDegrees = vars.headingDegrees
	local metric = vars.metric
	local alt = vars.alt
	local ref = vars.ref -- vec2/vec3 will be handled in routines.getBRString
	local text = vars.text
	local displayTime = vars.displayTime
	local msgFor = vars.msgFor

	local s = routines.getLeadingBRString{units = units, heading = heading, radius = radius, headingDegrees = headingDegrees, metric = metric, alt = alt, ref = ref}
	local newText
	if string.find(text, '%%s') then  -- look for %s
		newText = string.format(text, s)  -- insert the coordinates into the message
	else  -- else, just append to the end.
		newText = text .. s
	end

	routines.message.add{
		text = newText,
		displayTime = displayTime,
		msgFor = msgFor
	}
end


function 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


function routines.IsPartOfGroupInZones( CargoGroup, LandingZones )
--trace.f()

	local CurrentZoneID = nil

	if CargoGroup then
		local CargoUnits = CargoGroup:getUnits()
		for CargoUnitID, CargoUnit in pairs( CargoUnits ) do
			if CargoUnit and CargoUnit:getLife() >= 1.0 then
				CurrentZoneID = routines.IsUnitInZones( CargoUnit, LandingZones )
				if CurrentZoneID then
					break
				end
			end
		end
	end

--trace.r( "", "", { CurrentZoneID } )
	return CurrentZoneID
end



function routines.IsUnitInZones( TransportUnit, LandingZones )
--trace.f("", "routines.IsUnitInZones" )

    local TransportZoneResult = nil
	local TransportZonePos = nil
	local TransportZone = nil

    -- fill-up some local variables to support further calculations to determine location of units within the zone.
	if TransportUnit then
		local TransportUnitPos = TransportUnit:getPosition().p
		if type( LandingZones ) == "table" then
			for LandingZoneID, LandingZoneName in pairs( LandingZones ) do
				TransportZone = trigger.misc.getZone( LandingZoneName )
				if TransportZone then
					TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z}
					if  ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then
						TransportZoneResult = LandingZoneID
						break
					end
				end
			end
		else
			TransportZone = trigger.misc.getZone( LandingZones )
			TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z}
			if  ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then
				TransportZoneResult = 1
			end
		end
		if TransportZoneResult then
			--trace.i( "routines", "TransportZone:" .. TransportZoneResult )
		else
			--trace.i( "routines", "TransportZone:nil logic" )
		end
		return TransportZoneResult
	else
		--trace.i( "routines", "TransportZone:nil hard" )
		return nil
	end
end

function routines.IsUnitNearZonesRadius( TransportUnit, LandingZones, ZoneRadius )
--trace.f("", "routines.IsUnitInZones" )

  local TransportZoneResult = nil
  local TransportZonePos = nil
  local TransportZone = nil

    -- fill-up some local variables to support further calculations to determine location of units within the zone.
  if TransportUnit then
    local TransportUnitPos = TransportUnit:getPosition().p
    if type( LandingZones ) == "table" then
      for LandingZoneID, LandingZoneName in pairs( LandingZones ) do
        TransportZone = trigger.misc.getZone( LandingZoneName )
        if TransportZone then
          TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z}
          if  ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then
            TransportZoneResult = LandingZoneID
            break
          end
        end
      end
    else
      TransportZone = trigger.misc.getZone( LandingZones )
      TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z}
      if  ((( TransportUnitPos.x - TransportZonePos.x)^2 + (TransportUnitPos.z - TransportZonePos.z)^2)^0.5 <= ZoneRadius ) then
        TransportZoneResult = 1
      end
    end
    if TransportZoneResult then
      --trace.i( "routines", "TransportZone:" .. TransportZoneResult )
    else
      --trace.i( "routines", "TransportZone:nil logic" )
    end
    return TransportZoneResult
  else
    --trace.i( "routines", "TransportZone:nil hard" )
    return nil
  end
end


function routines.IsStaticInZones( TransportStatic, LandingZones )
--trace.f()

    local TransportZoneResult = nil
	local TransportZonePos = nil
	local TransportZone = nil

    -- fill-up some local variables to support further calculations to determine location of units within the zone.
    local TransportStaticPos = TransportStatic:getPosition().p
	if type( LandingZones ) == "table" then
		for LandingZoneID, LandingZoneName in pairs( LandingZones ) do
			TransportZone = trigger.misc.getZone( LandingZoneName )
			if TransportZone then
				TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z}
				if  ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then
					TransportZoneResult = LandingZoneID
					break
				end
			end
		end
	else
		TransportZone = trigger.misc.getZone( LandingZones )
		TransportZonePos = {radius = TransportZone.radius, x = TransportZone.point.x, y = TransportZone.point.y, z = TransportZone.point.z}
		if  ((( TransportStaticPos.x - TransportZonePos.x)^2 + (TransportStaticPos.z - TransportZonePos.z)^2)^0.5 <= TransportZonePos.radius) then
			TransportZoneResult = 1
		end
	end

--trace.r( "", "", { TransportZoneResult } )
    return TransportZoneResult
end


function routines.IsUnitInRadius( CargoUnit, ReferencePosition, Radius )
--trace.f()

  local Valid = true

  -- fill-up some local variables to support further calculations to determine location of units within the zone.
  local CargoPos = CargoUnit:getPosition().p
  local ReferenceP = ReferencePosition.p

  if  (((CargoPos.x - ReferenceP.x)^2 + (CargoPos.z - ReferenceP.z)^2)^0.5 <= Radius) then
  else
    Valid = false
  end

  return Valid
end

function routines.IsPartOfGroupInRadius( CargoGroup, ReferencePosition, Radius )
--trace.f()

  local Valid = true

  Valid = routines.ValidateGroup( CargoGroup, "CargoGroup", Valid )

  -- fill-up some local variables to support further calculations to determine location of units within the zone
  local CargoUnits = CargoGroup:getUnits()
  for CargoUnitId, CargoUnit in pairs( CargoUnits ) do
    local CargoUnitPos = CargoUnit:getPosition().p
--    env.info( 'routines.IsPartOfGroupInRadius: CargoUnitPos.x = ' .. CargoUnitPos.x .. ' CargoUnitPos.z = ' .. CargoUnitPos.z )
    local ReferenceP = ReferencePosition.p
--    env.info( 'routines.IsPartOfGroupInRadius: ReferenceGroupPos.x = ' .. ReferenceGroupPos.x .. ' ReferenceGroupPos.z = ' .. ReferenceGroupPos.z )

    if  ((( CargoUnitPos.x - ReferenceP.x)^2 + (CargoUnitPos.z - ReferenceP.z)^2)^0.5 <= Radius) then
    else
      Valid = false
      break
    end
  end

  return Valid
end


function routines.ValidateString( Variable, VariableName, Valid )
--trace.f()

  if  type( Variable ) == "string" then
    if Variable == "" then
      error( "routines.ValidateString: error: " .. VariableName .. " must be filled out!" )
      Valid = false
    end
  else
    error( "routines.ValidateString: error: " .. VariableName .. " is not a string." )
    Valid = false
  end

--trace.r( "", "", { Valid } )
  return Valid
end

function routines.ValidateNumber( Variable, VariableName, Valid )
--trace.f()

  if  type( Variable ) == "number" then
  else
    error( "routines.ValidateNumber: error: " .. VariableName .. " is not a number." )
    Valid = false
  end

--trace.r( "", "", { Valid } )
  return Valid

end

function routines.ValidateGroup( Variable, VariableName, Valid )
--trace.f()

	if Variable == nil then
		error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" )
		Valid = false
	end

--trace.r( "", "", { Valid } )
	return Valid
end

function routines.ValidateZone( LandingZones, VariableName, Valid )
--trace.f()

	if LandingZones == nil then
		error( "routines.ValidateGroup: error: " .. VariableName .. " is a nil value!" )
		Valid = false
	end

	if type( LandingZones ) == "table" then
		for LandingZoneID, LandingZoneName in pairs( LandingZones ) do
			if trigger.misc.getZone( LandingZoneName ) == nil then
				error( "routines.ValidateGroup: error: Zone " .. LandingZoneName .. " does not exist!" )
				Valid = false
				break
			end
		end
	else
		if trigger.misc.getZone( LandingZones ) == nil then
			error( "routines.ValidateGroup: error: Zone " .. LandingZones .. " does not exist!" )
			Valid = false
		end
	end

--trace.r( "", "", { Valid } )
	return Valid
end

function routines.ValidateEnumeration( Variable, VariableName, Enum, Valid )
--trace.f()

  local ValidVariable = false

  for EnumId, EnumData in pairs( Enum ) do
    if Variable == EnumData then
      ValidVariable = true
      break
    end
  end

  if  ValidVariable then
  else
    error( 'TransportValidateEnum: " .. VariableName .. " is not a valid type.' .. Variable )
    Valid = false
  end

--trace.r( "", "", { Valid } )
  return Valid
end

function routines.getGroupRoute(groupIdent, task)   -- same as getGroupPoints but returns speed and formation type along with vec2 of point}
		-- refactor to search by groupId and allow groupId and groupName as inputs
	local gpId = groupIdent
	if type(groupIdent) == 'string' and not tonumber(groupIdent) then
		gpId = _DATABASE.Templates.Groups[groupIdent].groupId
	end
	
	for coa_name, coa_data in pairs(env.mission.coalition) do
		if (coa_name == 'red' or coa_name == 'blue') and type(coa_data) == 'table' then			
			if coa_data.country then --there is a country table
				for cntry_id, cntry_data in pairs(coa_data.country) do
					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" then	-- only these types have points						
							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!				
								for group_num, group_data in pairs(obj_type_data.group) do		
									if group_data and group_data.groupId == gpId  then -- this is the group we are looking for
										if group_data.route and group_data.route.points and #group_data.route.points > 0 then
											local points = {}
											
											for point_num, point in pairs(group_data.route.points) do
												local routeData = {}
												if not point.point then
													routeData.x = point.x
													routeData.y = point.y
												else
													routeData.point = point.point  --it's possible that the ME could move to the point = Vec2 notation.
												end
												routeData.form = point.action
												routeData.speed = point.speed
												routeData.alt = point.alt
												routeData.alt_type = point.alt_type
												routeData.airdromeId = point.airdromeId
												routeData.helipadId = point.helipadId
												routeData.type = point.type
												routeData.action = point.action
												if task then
													routeData.task = point.task
												end
												points[point_num] = routeData
											end
											
											return points
										end
										return
									end  --if group_data and group_data.name and group_data.name == 'groupname'
								end --for group_num, group_data 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 --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
end

routines.ground.patrolRoute = function(vars)
	
	
	local tempRoute = {}
	local useRoute = {}
	local gpData = vars.gpData
	if type(gpData) == 'string' then
		gpData = Group.getByName(gpData)
	end
	
	local useGroupRoute 
	if not vars.useGroupRoute then
		useGroupRoute = vars.gpData
	else
		useGroupRoute = vars.useGroupRoute
	end
	local routeProvided = false
	if not vars.route then
		if useGroupRoute then
			tempRoute = routines.getGroupRoute(useGroupRoute)
		end
	else
		useRoute = vars.route
		local posStart = routines.getLeadPos(gpData)
		useRoute[1] = routines.ground.buildWP(posStart, useRoute[1].action, useRoute[1].speed)
		routeProvided = true
	end
	
	
	local overRideSpeed = vars.speed or 'default'
	local pType = vars.pType 
	local offRoadForm = vars.offRoadForm or 'default'
	local onRoadForm = vars.onRoadForm or 'default'
		
	if routeProvided == false and #tempRoute > 0 then
		local posStart = routines.getLeadPos(gpData)
		
		
		useRoute[#useRoute + 1] = routines.ground.buildWP(posStart, offRoadForm, overRideSpeed)
		for i = 1, #tempRoute do
			local tempForm = tempRoute[i].action
			local tempSpeed = tempRoute[i].speed
			
			if offRoadForm == 'default' then
				tempForm = tempRoute[i].action
			end
			if onRoadForm == 'default' then
				onRoadForm = 'On Road'
			end
			if (string.lower(tempRoute[i].action) == 'on road' or  string.lower(tempRoute[i].action) == 'onroad' or string.lower(tempRoute[i].action) == 'on_road') then
				tempForm = onRoadForm
			else
				tempForm = offRoadForm
			end
			
			if type(overRideSpeed) == 'number' then
				tempSpeed = overRideSpeed
			end
			
			
			useRoute[#useRoute + 1] = routines.ground.buildWP(tempRoute[i], tempForm, tempSpeed)
		end
			
		if pType and string.lower(pType) == 'doubleback' then
			local curRoute = routines.utils.deepCopy(useRoute)
			for i = #curRoute, 2, -1 do
				useRoute[#useRoute + 1] = routines.ground.buildWP(curRoute[i], curRoute[i].action, curRoute[i].speed)
			end
		end
		
		useRoute[1].action = useRoute[#useRoute].action -- make it so the first WP matches the last WP
	end
	
	local cTask3 = {}
	local newPatrol = {}
	newPatrol.route = useRoute
	newPatrol.gpData = gpData:getName()
	cTask3[#cTask3 + 1] = 'routines.ground.patrolRoute('
	cTask3[#cTask3 + 1] = routines.utils.oneLineSerialize(newPatrol)
	cTask3[#cTask3 + 1] = ')'
	cTask3 = table.concat(cTask3)
	local tempTask = {
		id = 'WrappedAction', 
		params = { 
			action = {
				id = 'Script',
				params = {
					command = cTask3, 
					
				},
			},
		},
	}

		
	useRoute[#useRoute].task = tempTask
	routines.goRoute(gpData, useRoute)
	
	return
end

routines.ground.patrol = function(gpData, pType, form, speed)
	local vars = {}
	
	if type(gpData) == 'table' and gpData:getName() then
		gpData = gpData:getName()
	end
	
	vars.useGroupRoute = gpData
	vars.gpData = gpData
	vars.pType = pType
	vars.offRoadForm = form
	vars.speed = speed
	
	routines.ground.patrolRoute(vars)

	return
end

function routines.GetUnitHeight( CheckUnit )
--trace.f( "routines" )

	local UnitPoint = CheckUnit:getPoint()
	local UnitPosition = { x = UnitPoint.x, y = UnitPoint.z }
	local UnitHeight = UnitPoint.y

	local LandHeight = land.getHeight( UnitPosition )

	--env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight ))

	--trace.f( "routines", "Unit Height = " .. UnitHeight - LandHeight )
	
	return UnitHeight - LandHeight

end



Su34Status = { status = {} }
boardMsgRed = { statusMsg = "" }
boardMsgAll = { timeMsg = "" }
SpawnSettings = {}
Su34MenuPath = {}
Su34Menus = 0


function Su34AttackCarlVinson(groupName)
--trace.menu("", "Su34AttackCarlVinson")
	local groupSu34 = Group.getByName( groupName )
	local controllerSu34 = groupSu34.getController(groupSu34)
	local groupCarlVinson = Group.getByName("US Carl Vinson #001")
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE )
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE )
	if groupCarlVinson ~= nil then
		controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupCarlVinson:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}})
	end
	Su34Status.status[groupName] = 1
	MessageToRed( string.format('%s: ',groupName) .. 'Attacking carrier Carl Vinson. ', 10, 'RedStatus' .. groupName )
end

function Su34AttackWest(groupName)
--trace.f("","Su34AttackWest")
	local groupSu34 = Group.getByName( groupName )
	local controllerSu34 = groupSu34.getController(groupSu34)
	local groupShipWest1 = Group.getByName("US Ship West #001")
	local groupShipWest2 = Group.getByName("US Ship West #002")
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE )
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE )
	if groupShipWest1 ~= nil then
		controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}})
	end
	if groupShipWest2 ~= nil then
		controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipWest2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = true}})
	end
	Su34Status.status[groupName] = 2
	MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the west. ', 10, 'RedStatus' .. groupName )
end

function Su34AttackNorth(groupName)
--trace.menu("","Su34AttackNorth")
	local groupSu34 = Group.getByName( groupName )
	local controllerSu34 = groupSu34.getController(groupSu34)
	local groupShipNorth1 = Group.getByName("US Ship North #001")
	local groupShipNorth2 = Group.getByName("US Ship North #002")
	local groupShipNorth3 = Group.getByName("US Ship North #003")
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.OPEN_FIRE )
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE )
	if groupShipNorth1 ~= nil then
		controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth1:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}})
	end
	if groupShipNorth2 ~= nil then
		controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth2:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}})
	end
	if groupShipNorth3 ~= nil then
		controllerSu34.pushTask(controllerSu34,{id = 'AttackGroup', params = { groupId = groupShipNorth3:getID(), expend = AI.Task.WeaponExpend.ALL, attackQtyLimit = false}})
	end
	Su34Status.status[groupName] = 3
	MessageToRed( string.format('%s: ',groupName) .. 'Attacking invading ships in the north. ', 10, 'RedStatus' .. groupName )
end

function Su34Orbit(groupName)
--trace.menu("","Su34Orbit")
	local groupSu34 = Group.getByName( groupName )
	local controllerSu34 = groupSu34:getController()
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD )
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.EVADE_FIRE )
	controllerSu34:pushTask( {id = 'ControlledTask', params = { task = { id = 'Orbit', params = { pattern = AI.Task.OrbitPattern.RACE_TRACK } }, stopCondition = { duration = 600 } } } )
	Su34Status.status[groupName] = 4
	MessageToRed( string.format('%s: ',groupName) .. 'In orbit and awaiting further instructions. ', 10, 'RedStatus' .. groupName )
end

function Su34TakeOff(groupName)
--trace.menu("","Su34TakeOff")
	local groupSu34 = Group.getByName( groupName )
	local controllerSu34 = groupSu34:getController()
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD )
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE )
	Su34Status.status[groupName] = 8
	MessageToRed( string.format('%s: ',groupName) .. 'Take-Off. ', 10, 'RedStatus' .. groupName )
end

function Su34Hold(groupName)
--trace.menu("","Su34Hold")
	local groupSu34 = Group.getByName( groupName )
	local controllerSu34 = groupSu34:getController()
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.ROE, AI.Option.Air.val.ROE.WEAPON_HOLD )
	controllerSu34.setOption( controllerSu34, AI.Option.Air.id.REACTION_ON_THREAT, AI.Option.Air.val.REACTION_ON_THREAT.BYPASS_AND_ESCAPE )
	Su34Status.status[groupName] = 5
	MessageToRed( string.format('%s: ',groupName) .. 'Holding Weapons. ', 10, 'RedStatus' .. groupName )
end

function Su34RTB(groupName)
--trace.menu("","Su34RTB")
	Su34Status.status[groupName] = 6
	MessageToRed( string.format('%s: ',groupName) .. 'Return to Krasnodar. ', 10, 'RedStatus' .. groupName )
end

function Su34Destroyed(groupName)
--trace.menu("","Su34Destroyed")
	Su34Status.status[groupName] = 7
	MessageToRed( string.format('%s: ',groupName) .. 'Destroyed. ', 30, 'RedStatus' .. groupName )
end

function GroupAlive( groupName )
--trace.menu("","GroupAlive")
	local groupTest = Group.getByName( groupName )

	local groupExists = false

	if groupTest then
		groupExists = groupTest:isExist()
	end

	--trace.r( "", "", { groupExists } )
	return groupExists
end

function Su34IsDead()
--trace.f()

end

function Su34OverviewStatus()
--trace.menu("","Su34OverviewStatus")
	local msg = ""
	local currentStatus = 0
	local Exists = false

	for groupName, currentStatus in pairs(Su34Status.status) do

		env.info(('Su34 Overview Status: GroupName = ' .. groupName ))
		Alive = GroupAlive( groupName )

		if Alive then
			if currentStatus == 1 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Attacking carrier Carl Vinson. "
			elseif currentStatus == 2 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Attacking supporting ships in the west. "
			elseif currentStatus == 3 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Attacking invading ships in the north. "
			elseif currentStatus == 4 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "In orbit and awaiting further instructions. "
			elseif currentStatus == 5 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Holding Weapons. "
			elseif currentStatus == 6 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Return to Krasnodar. "
			elseif currentStatus == 7 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Destroyed. "
			elseif currentStatus == 8 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Take-Off. "
			end
		else
			if currentStatus == 7 then
				msg = msg .. string.format("%s: ",groupName)
				msg = msg .. "Destroyed. "
			else
				Su34Destroyed(groupName)
			end
		end
	end

	boardMsgRed.statusMsg = msg
end


function UpdateBoardMsg()
--trace.f()
	Su34OverviewStatus()
	MessageToRed( boardMsgRed.statusMsg, 15, 'RedStatus' )
end

function MusicReset( flg )
--trace.f()
	trigger.action.setUserFlag(95,flg)
end

function PlaneActivate(groupNameFormat, flg)
--trace.f()
	local groupName = groupNameFormat .. string.format("#%03d", trigger.misc.getUserFlag(flg))
	--trigger.action.outText(groupName,10)
	trigger.action.activateGroup(Group.getByName(groupName))
end

function Su34Menu(groupName)
--trace.f()

	--env.info(( 'Su34Menu(' .. groupName .. ')' ))
	local groupSu34 = Group.getByName( groupName )

	if Su34Status.status[groupName] == 1 or
	   Su34Status.status[groupName] == 2 or
	   Su34Status.status[groupName] == 3 or
	   Su34Status.status[groupName] == 4 or
	   Su34Status.status[groupName] == 5 then
		if Su34MenuPath[groupName] == nil then
			if planeMenuPath == nil then
				planeMenuPath = missionCommands.addSubMenuForCoalition(
					coalition.side.RED,
					"SU-34 anti-ship flights",
					nil
				)
			end
			Su34MenuPath[groupName] = missionCommands.addSubMenuForCoalition(
				coalition.side.RED,
				"Flight " .. groupName,
				planeMenuPath
			)

			missionCommands.addCommandForCoalition(
				coalition.side.RED,
				"Attack carrier Carl Vinson",
				Su34MenuPath[groupName],
				Su34AttackCarlVinson,
				groupName
			)

			missionCommands.addCommandForCoalition(
				coalition.side.RED,
				"Attack ships in the west",
				Su34MenuPath[groupName],
				Su34AttackWest,
				groupName
			)

			missionCommands.addCommandForCoalition(
				coalition.side.RED,
				"Attack ships in the north",
				Su34MenuPath[groupName],
				Su34AttackNorth,
				groupName
			)

			missionCommands.addCommandForCoalition(
				coalition.side.RED,
				"Hold position and await instructions",
				Su34MenuPath[groupName],
				Su34Orbit,
				groupName
			)

			missionCommands.addCommandForCoalition(
				coalition.side.RED,
				"Report status",
				Su34MenuPath[groupName],
				Su34OverviewStatus
			)
		end
	else
		if Su34MenuPath[groupName] then
			missionCommands.removeItemForCoalition(coalition.side.RED, Su34MenuPath[groupName])
		end
	end
end

--- Obsolete function, but kept to rework in framework.

function ChooseInfantry ( TeleportPrefixTable, TeleportMax )
--trace.f("Spawn")
	--env.info(( 'ChooseInfantry: ' ))

	TeleportPrefixTableCount = #TeleportPrefixTable
	TeleportPrefixTableIndex = math.random( 1, TeleportPrefixTableCount )

	--env.info(( 'ChooseInfantry: TeleportPrefixTableIndex = ' .. TeleportPrefixTableIndex .. ' TeleportPrefixTableCount = ' .. TeleportPrefixTableCount  .. ' TeleportMax = ' .. TeleportMax ))

	local TeleportFound = false
	local TeleportLoop = true
	local Index = TeleportPrefixTableIndex
	local TeleportPrefix = ''

	while TeleportLoop do
		TeleportPrefix = TeleportPrefixTable[Index]
		if SpawnSettings[TeleportPrefix] then
			if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then
				SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1
				TeleportFound = true
			else
				TeleportFound = false
			end
		else
			SpawnSettings[TeleportPrefix] = {}
			SpawnSettings[TeleportPrefix]['SpawnCount'] = 0
			TeleportFound = true
		end
		if TeleportFound then
			TeleportLoop = false
		else
			if Index < TeleportPrefixTableCount then
				Index = Index + 1
			else
				TeleportLoop = false
			end
		end
		--env.info(( 'ChooseInfantry: Loop 1 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index ))
	end

	if TeleportFound == false then
		TeleportLoop = true
		Index = 1
		while TeleportLoop do
			TeleportPrefix = TeleportPrefixTable[Index]
			if SpawnSettings[TeleportPrefix] then
				if SpawnSettings[TeleportPrefix]['SpawnCount'] - 1 < TeleportMax then
					SpawnSettings[TeleportPrefix]['SpawnCount'] = SpawnSettings[TeleportPrefix]['SpawnCount'] + 1
					TeleportFound = true
				else
					TeleportFound = false
				end
			else
				SpawnSettings[TeleportPrefix] = {}
				SpawnSettings[TeleportPrefix]['SpawnCount'] = 0
				TeleportFound = true
			end
			if TeleportFound then
				TeleportLoop = false
			else
				if Index < TeleportPrefixTableIndex then
					Index = Index + 1
				else
					TeleportLoop = false
				end
			end
		--env.info(( 'ChooseInfantry: Loop 2 - TeleportPrefix = ' .. TeleportPrefix .. ' Index = ' .. Index ))
		end
	end

	local TeleportGroupName = ''
	if TeleportFound == true then
		TeleportGroupName = TeleportPrefix .. string.format("#%03d", SpawnSettings[TeleportPrefix]['SpawnCount'] )
	else
		TeleportGroupName = ''
	end

	--env.info(('ChooseInfantry: TeleportGroupName = ' .. TeleportGroupName ))
	--env.info(('ChooseInfantry: return'))

	return TeleportGroupName
end

SpawnedInfantry = 0

function LandCarrier ( CarrierGroup, LandingZonePrefix )
--trace.f()
	--env.info(( 'LandCarrier: ' ))
	--env.info(( 'LandCarrier: CarrierGroup = ' .. CarrierGroup:getName() ))
	--env.info(( 'LandCarrier: LandingZone = ' .. LandingZonePrefix ))

	local controllerGroup = CarrierGroup:getController()

	local LandingZone = trigger.misc.getZone(LandingZonePrefix)
	local LandingZonePos = {}
	LandingZonePos.x = LandingZone.point.x + math.random(LandingZone.radius * -1, LandingZone.radius)
	LandingZonePos.y = LandingZone.point.z + math.random(LandingZone.radius * -1, LandingZone.radius)

	controllerGroup:pushTask( { id = 'Land', params = { point = LandingZonePos, durationFlag = true, duration = 10 } } )

	--env.info(( 'LandCarrier: end' ))
end

EscortCount = 0
function EscortCarrier ( CarrierGroup, EscortPrefix, EscortLastWayPoint, EscortEngagementDistanceMax, EscortTargetTypes )
--trace.f()
	--env.info(( 'EscortCarrier: ' ))
	--env.info(( 'EscortCarrier: CarrierGroup = ' .. CarrierGroup:getName() ))
	--env.info(( 'EscortCarrier: EscortPrefix = ' .. EscortPrefix ))

	local CarrierName = CarrierGroup:getName()

	local EscortMission = {}
	local CarrierMission = {}

	local EscortMission =  SpawnMissionGroup( EscortPrefix )
	local CarrierMission = SpawnMissionGroup( CarrierGroup:getName() )

	if EscortMission ~= nil and CarrierMission ~= nil then

		EscortCount = EscortCount + 1
		EscortMissionName = string.format( EscortPrefix .. '#Escort %s', CarrierName )
		EscortMission.name = EscortMissionName
		EscortMission.groupId = nil
		EscortMission.lateActivation = false
		EscortMission.taskSelected = false

		local EscortUnits = #EscortMission.units
		for u = 1, EscortUnits do
			EscortMission.units[u].name = string.format( EscortPrefix .. '#Escort %s %02d', CarrierName, u )
			EscortMission.units[u].unitId = nil
		end


		EscortMission.route.points[1].task =  { id = "ComboTask",
                                                params =
                                                {
                                                    tasks =
                                                    {
                                                        [1] =
                                                        {
                                                            enabled = true,
                                                            auto = false,
                                                            id = "Escort",
                                                            number = 1,
                                                            params =
                                                            {
                                                                lastWptIndexFlagChangedManually = false,
                                                                groupId = CarrierGroup:getID(),
                                                                lastWptIndex = nil,
                                                                lastWptIndexFlag = false,
																engagementDistMax = EscortEngagementDistanceMax,
																targetTypes = EscortTargetTypes,
                                                                pos =
                                                                {
                                                                    y = 20,
                                                                    x = 20,
                                                                    z = 0,
                                                                } -- end of ["pos"]
                                                            } -- end of ["params"]
                                                        } -- end of [1]
                                                    } -- end of ["tasks"]
                                                } -- end of ["params"]
                                            } -- end of ["task"]

		SpawnGroupAdd( EscortPrefix, EscortMission )

	end
end

function SendMessageToCarrier( CarrierGroup, CarrierMessage )
--trace.f()

	if CarrierGroup ~= nil then
		MessageToGroup( CarrierGroup, CarrierMessage, 30, 'Carrier/' .. CarrierGroup:getName() )
	end

end

function MessageToGroup( MsgGroup, MsgText, MsgTime, MsgName )
--trace.f()

	if type(MsgGroup) == 'string' then
		--env.info( 'MessageToGroup: Converted MsgGroup string "' .. MsgGroup .. '" into a Group structure.' )
		MsgGroup = Group.getByName( MsgGroup )
	end

	if MsgGroup ~= nil then
		local MsgTable = {}
		MsgTable.text = MsgText
		MsgTable.displayTime = MsgTime
		MsgTable.msgFor = { units = { MsgGroup:getUnits()[1]:getName() } }
		MsgTable.name = MsgName
		--routines.message.add( MsgTable )
		--env.info(('MessageToGroup: Message sent to ' .. MsgGroup:getUnits()[1]:getName() .. ' -> ' .. MsgText ))
	end
end

function MessageToUnit( UnitName, MsgText, MsgTime, MsgName )
--trace.f()

	if UnitName ~= nil then
		local MsgTable = {}
		MsgTable.text = MsgText
		MsgTable.displayTime = MsgTime
		MsgTable.msgFor = { units = { UnitName } }
		MsgTable.name = MsgName
		--routines.message.add( MsgTable )
	end
end

function MessageToAll( MsgText, MsgTime, MsgName )
--trace.f()

	MESSAGE:New( MsgText, MsgTime, "Message" ):ToCoalition( coalition.side.RED ):ToCoalition( coalition.side.BLUE )
end

function MessageToRed( MsgText, MsgTime, MsgName )
--trace.f()

	MESSAGE:New( MsgText, MsgTime, "To Red Coalition" ):ToCoalition( coalition.side.RED )
end

function MessageToBlue( MsgText, MsgTime, MsgName )
--trace.f()

	MESSAGE:New( MsgText, MsgTime, "To Blue Coalition" ):ToCoalition( coalition.side.BLUE )
end

function getCarrierHeight( CarrierGroup )
--trace.f()

	if CarrierGroup ~= nil then
		if table.getn(CarrierGroup:getUnits()) == 1 then
			local CarrierUnit = CarrierGroup:getUnits()[1]
			local CurrentPoint = CarrierUnit:getPoint()

			local CurrentPosition = { x = CurrentPoint.x, y = CurrentPoint.z }
			local CarrierHeight = CurrentPoint.y

			local LandHeight = land.getHeight( CurrentPosition )

			--env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight ))

			return CarrierHeight - LandHeight
		else
			return 999999
		end
	else
		return 999999
	end

end

function GetUnitHeight( CheckUnit )
--trace.f()

	local UnitPoint = CheckUnit:getPoint()
	local UnitPosition = { x = CurrentPoint.x, y = CurrentPoint.z }
	local UnitHeight = CurrentPoint.y

	local LandHeight = land.getHeight( CurrentPosition )

	--env.info(( 'CarrierHeight: LandHeight = ' .. LandHeight .. ' CarrierHeight = ' .. CarrierHeight ))

	return UnitHeight - LandHeight

end


_MusicTable = {}
_MusicTable.Files = {}
_MusicTable.Queue = {}
_MusicTable.FileCnt = 0


function MusicRegister( SndRef, SndFile, SndTime )
--trace.f()

	env.info(( 'MusicRegister: SndRef = ' .. SndRef ))
	env.info(( 'MusicRegister: SndFile = ' .. SndFile ))
	env.info(( 'MusicRegister: SndTime = ' .. SndTime ))


	_MusicTable.FileCnt = _MusicTable.FileCnt + 1

	_MusicTable.Files[_MusicTable.FileCnt] = {}
	_MusicTable.Files[_MusicTable.FileCnt].Ref = SndRef
	_MusicTable.Files[_MusicTable.FileCnt].File = SndFile
	_MusicTable.Files[_MusicTable.FileCnt].Time = SndTime

	if not _MusicTable.Function then
		_MusicTable.Function = routines.scheduleFunction( MusicScheduler, { }, timer.getTime() + 10, 10)
	end

end

function MusicToPlayer( SndRef, PlayerName, SndContinue )
--trace.f()

	--env.info(( 'MusicToPlayer: SndRef = ' .. SndRef  ))

	local PlayerUnits = AlivePlayerUnits()
	for PlayerUnitIdx, PlayerUnit in pairs(PlayerUnits) do
		local PlayerUnitName = PlayerUnit:getPlayerName()
		--env.info(( 'MusicToPlayer: PlayerUnitName = ' .. PlayerUnitName  ))
		if PlayerName == PlayerUnitName then
			PlayerGroup = PlayerUnit:getGroup()
			if PlayerGroup then
				--env.info(( 'MusicToPlayer: PlayerGroup = ' .. PlayerGroup:getName() ))
				MusicToGroup( SndRef, PlayerGroup, SndContinue )
			end
			break
		end
	end

	--env.info(( 'MusicToPlayer: end'  ))

end

function MusicToGroup( SndRef, SndGroup, SndContinue )
--trace.f()

	--env.info(( 'MusicToGroup: SndRef = ' .. SndRef  ))

	if SndGroup ~= nil then
		if _MusicTable and _MusicTable.FileCnt > 0 then
			if SndGroup:isExist() then
				if MusicCanStart(SndGroup:getUnit(1):getPlayerName()) then
					--env.info(( 'MusicToGroup: OK for Sound.'  ))
					local SndIdx = 0
					if SndRef == '' then
						--env.info(( 'MusicToGroup: SndRef as empty. Queueing at random.'  ))
						SndIdx = math.random( 1, _MusicTable.FileCnt )
					else
						for SndIdx = 1, _MusicTable.FileCnt do
							if _MusicTable.Files[SndIdx].Ref == SndRef then
								break
							end
						end
					end
					--env.info(( 'MusicToGroup: SndIdx =  ' .. SndIdx ))
					--env.info(( 'MusicToGroup: Queueing Music ' .. _MusicTable.Files[SndIdx].File .. ' for Group ' ..  SndGroup:getID() ))
					trigger.action.outSoundForGroup( SndGroup:getID(), _MusicTable.Files[SndIdx].File )
					MessageToGroup( SndGroup, 'Playing ' .. _MusicTable.Files[SndIdx].File, 15, 'Music-' .. SndGroup:getUnit(1):getPlayerName() )

					local SndQueueRef = SndGroup:getUnit(1):getPlayerName()
					if _MusicTable.Queue[SndQueueRef] == nil then
						_MusicTable.Queue[SndQueueRef] = {}
					end
					_MusicTable.Queue[SndQueueRef].Start = timer.getTime()
					_MusicTable.Queue[SndQueueRef].PlayerName = SndGroup:getUnit(1):getPlayerName()
					_MusicTable.Queue[SndQueueRef].Group = SndGroup
					_MusicTable.Queue[SndQueueRef].ID = SndGroup:getID()
					_MusicTable.Queue[SndQueueRef].Ref = SndIdx
					_MusicTable.Queue[SndQueueRef].Continue = SndContinue
					_MusicTable.Queue[SndQueueRef].Type = Group
				end
			end
		end
	end
end

function MusicCanStart(PlayerName)
--trace.f()

	--env.info(( 'MusicCanStart:' ))

	local MusicOut = false

	if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0  then
		--env.info(( 'MusicCanStart: PlayerName = ' .. PlayerName ))
		local PlayerFound = false
		local MusicStart = 0
		local MusicTime = 0
		for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do
			if SndQueue.PlayerName == PlayerName then
				PlayerFound = true
				MusicStart = SndQueue.Start
				MusicTime = _MusicTable.Files[SndQueue.Ref].Time
				break
			end
		end
		if PlayerFound then
			--env.info(( 'MusicCanStart: MusicStart = ' .. MusicStart ))
			--env.info(( 'MusicCanStart: MusicTime = ' .. MusicTime ))
			--env.info(( 'MusicCanStart: timer.getTime() = ' .. timer.getTime() ))

			if MusicStart + MusicTime <= timer.getTime() then
				MusicOut = true
			end
		else
			MusicOut = true
		end
	end

	if MusicOut then
		--env.info(( 'MusicCanStart: true' ))
	else
		--env.info(( 'MusicCanStart: false' ))
	end

	return MusicOut
end

function MusicScheduler()
--trace.scheduled("", "MusicScheduler")

	--env.info(( 'MusicScheduler:' ))
	if _MusicTable['Queue'] ~= nil and _MusicTable.FileCnt > 0  then
		--env.info(( 'MusicScheduler: Walking Sound Queue.'))
		for SndQueueIdx, SndQueue in pairs( _MusicTable.Queue ) do
			if SndQueue.Continue then
				if MusicCanStart(SndQueue.PlayerName) then
					--env.info(('MusicScheduler: MusicToGroup'))
					MusicToPlayer( '', SndQueue.PlayerName, true )
				end
			end
		end
	end

end


env.info(( 'Init: Scripts Loaded v1.1' ))

--- This module contains derived utilities taken from the MIST framework, 
-- which are excellent tools to be reused in an OO environment!.
-- 
-- ### Authors: 
-- 
--   * Grimes : Design & Programming of the MIST framework.
--   
-- ### Contributions:
-- 
--   * FlightControl : Rework to OO framework 
-- 
-- @module Utils
-- @image MOOSE.JPG


--- @type SMOKECOLOR
-- @field Green
-- @field Red
-- @field White
-- @field Orange
-- @field Blue
 
SMOKECOLOR = trigger.smokeColor -- #SMOKECOLOR

--- @type FLARECOLOR
-- @field Green
-- @field Red
-- @field White
-- @field Yellow

FLARECOLOR = trigger.flareColor -- #FLARECOLOR

--- Big smoke preset enum.
-- @type BIGSMOKEPRESET
BIGSMOKEPRESET = {
  SmallSmokeAndFire=0,
  MediumSmokeAndFire=1,
  LargeSmokeAndFire=2,
  HugeSmokeAndFire=3,
  SmallSmoke=4,
  MediumSmoke=5,
  LargeSmoke=6,
  HugeSmoke=7,
}

--- Utilities static class.
-- @type UTILS
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 not 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


--from http://lua-users.org/wiki/CopyTable
UTILS.DeepCopy = function(object)
  local lookup_table = {}
  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


-- porting in Slmod's serialize_slmod2
UTILS.OneLineSerialize = function( tbl )  -- serialization of a table all on a single line, no comments, made to replace old get_table_string function

  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] = routines.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] = routines.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 ' .. routines.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

--porting in Slmod's "safestring" basic 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) == 'table') or (type(s) == 'userdata') ) then
      return tostring(s)
    elseif type(s) == 'string' then
      s = string.format('%q', s)
      return s
    end
  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.MetersToFeet = function(meters)
  return meters/0.3048
end

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

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

UTILS.KnotsToKmph = 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

UTILS.MpsToMiph = function( mps )
  return mps / 0.44704
end

UTILS.MpsToKnots = function( mps )
  return mps * 3600 / 1852
end

UTILS.KnotsToMps = function( knots )
  return knots * 1852 / 3600
end

UTILS.CelciusToFarenheit = function( Celcius )
  return Celcius * 9/5 + 32 
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.
--      secFrmtStr = '%0' .. width .. '.' .. acc .. 'f'
--    end

    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

    return string.format('%03d', latDeg) .. ' ' .. string.format(minFrmtStr, latMin) .. '\'' .. latHemi .. '   '
     .. string.format('%03d', lonDeg) .. ' ' .. string.format(minFrmtStr, lonMin) .. '\'' .. lonHemi

  end
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
    return MGRS.UTMZone .. ' ' .. MGRS.MGRSDigraph .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Easting/(10^(5-acc)), 0))
           .. ' ' .. string.format('%0' .. acc .. 'd', UTILS.Round(MGRS.Northing/(10^(5-acc)), 0))
  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
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.
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

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

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

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.
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 seperators. C.f. http://stackoverflow.com/questions/1426954/split-string-in-lua
-- @param #string str Sting to split.
-- @param #string sep Speparator 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

--- Convert time in seconds to hours, minutes and seconds.
-- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function.
-- @return #string Time in format Hours:Minutes:Seconds+Days (HH:MM:SS+D).
function UTILS.SecondsToClock(seconds)
  
  -- Nil check.
  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 #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.
-- @param #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


--- 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 [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 [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

--- **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 debuggin 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 = true
local _TraceLevel = 1
local _TraceAll = false
local _TraceClass = {}
local _TraceClassMethod = {}

local _ClassID = 0

--- @type BASE
-- @field ClassName The name of the class.
-- @field ClassID The ID number of the class.
-- @field ClassNameAndID The name of the class concatenated with the ID number of the class.

--- 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 interiting BASE, soeach object created from derived class 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 @{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,
}


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

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

--- The Formation Class
-- @type FORMATION
-- @field Cone A cone formation.
FORMATION = {
  Cone = "Cone",
  Vee = "Vee" 
}



--- 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 = routines.utils.deepCopy( self ) -- Create a new self instance

	_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 )
	local Child = routines.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 is the Child class from which the Parent class needs to be retrieved.
-- @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 @{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 @{Event} processing Priority.
  function BASE:GetEventPriority()
    return self._.EventPriority or 5
  end
  
  --- Set the Class @{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 @{Event} processing Priority.
  -- @return 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 Event
  -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit.
  -- @return #BASE
  function BASE:HandleEvent( Event, EventFunction )
  
    self:EventDispatcher():OnEventGeneric( EventFunction, self, Event )
    
    return self
  end
  
  --- UnSubscribe to a DCS event.
  -- @param #BASE self
  -- @param Core.Event#EVENTS Event
  -- @return #BASE
  function BASE:UnHandleEvent( Event )
  
    self:EventDispatcher():RemoveEvent( self, Event )
    
    return self
  end
  
  -- Event handling function 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.
  -- @function [parent=#BASE] OnEventShot
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs whenever an object is hit by a weapon.
  -- 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.
  -- 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
  -- 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.
  -- 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
  -- 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.
  -- 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.
  -- initiator : The unit that is dead. 
  -- @function [parent=#BASE] OnEventDead
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an object is completely destroyed.
  -- initiator : The unit that is was destroyed. 
  -- @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.
  -- 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.
  -- 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 
  -- @function [parent=#BASE] OnEventMissionStart
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when a mission ends
  -- @function [parent=#BASE] OnEventMissionEnd
  -- @param #BASE self
  -- @param Core.Event#EVENTDATA EventData The EventData structure.

  --- Occurs when an aircraft is finished taking fuel.
  -- 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.
  -- 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.
  -- 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.
  -- 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.
  -- 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.
  -- 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.
  -- 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.
  -- initiator : The unit that is doing the shooing.
  -- 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.
  -- initiator : The unit that was doing the shooing. 
  -- @function [parent=#BASE] OnEventShootingEnd
  -- @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 )
	self:F( { EventTime, Initiator } )

	local Event = {
		id = world.event.S_EVENT_CRASH,
		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 )
  self:F( { EventTime, Initiator } )

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

  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

-- TODO: Complete DCS#Event structure.                       
--- 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)
  --self:F( { BaseEventCodes[event.id], event } )

	if self then
		for EventID, EventObject in pairs( self.Events ) do
			if EventObject.EventEnabled then
				--env.info( 'onEvent Table EventObject.Self = ' .. tostring(EventObject.Self) )
				--env.info( 'onEvent event.id = ' .. tostring(event.id) )
				--env.info( 'onEvent EventObject.Event = ' .. tostring(EventObject.Event) )
				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
						--self:T( { BaseEventCodes[event.id], event } )
						--EventObject.EventFunction( self, event )
					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 #table ... Optional arguments that can be given as part of scheduler. The arguments need to be given as a table { param1, param 2, ... }.
  -- @return #number The ScheduleID of the planned schedule.
  function BASE:ScheduleOnce( Start, SchedulerFunction, ... )
    self:F2( { Start } )
    self:T3( { ... } )
  
    local ObjectName = "-"
    ObjectName = self.ClassName .. self.ClassID
    
    self:F3( { "ScheduleOnce: ", ObjectName,  Start } )
    
    if not self.Scheduler then
      self.Scheduler = SCHEDULER:New( self )
    end
    
    self.Scheduler.SchedulerObject = self.Scheduler
    
    local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( 
      self, 
      SchedulerFunction,
      { ... },
      Start,
      nil,
      nil,
      nil
    )
    
    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 #number The ScheduleID 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
    
    self.Scheduler.SchedulerObject = self.Scheduler
    
    local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( 
      self, 
      SchedulerFunction,
      { ... },
      Start,
      Repeat,
      RandomizeFactor,
      Stop
    )
    
    self._.Schedules[#self._.Schedules+1] = ScheduleID
  
    return self._.Schedules[#self._.Schedules]
  end

  --- Stops the Schedule.
  -- @param #BASE self
  -- @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.
  function BASE:ScheduleStop( SchedulerFunction )
  
    self:F3( { "ScheduleStop:" } )
    
  _SCHEDULEDISPATCHER:Stop( self.Scheduler, self._.Schedules[SchedulerFunction] )
  end

end


--- Set a state or property of the Object given a Key and a Value.
-- Note that if the Object is destroyed, nillified 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, nillified 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

-- Trace section

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

--- 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 )
  _TraceOnOff = TraceOnOff
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
  self:E( "Tracing level " .. Level )
end

--- Trace all methods in MOOSE
-- @param #BASE self
-- @param #boolean TraceAll true = trace all methods in MOOSE.
function BASE:TraceAll( TraceAll )
  
  _TraceAll = TraceAll
  
  if _TraceAll then
    self:E( "Tracing all methods in MOOSE " )
  else
    self:E( "Switched off tracing all methods in MOOSE" )
  end
end

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

--- Set tracing for a specific method of  class
-- @param #BASE self
-- @param #string Class
-- @param #string Method
function BASE:TraceClassMethod( Class, Method )
  if not _TraceClassMethod[Class] then
    _TraceClassMethod[Class] = {}
    _TraceClassMethod[Class].Method = {}
  end
  _TraceClassMethod[Class].Method[Method] = true
  self:E( "Tracing method " .. Method .. " of class " .. Class )
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:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "F", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( 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 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 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 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:%20s%05d.%s" , LineCurrent, LineFrom, "T", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( 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 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 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 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:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "E", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) )
  else
    env.info( string.format( "%1s:%20s%05d(%s)" , "E", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( 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:%20s%05d.%s(%s)" , LineCurrent, LineFrom, "I", self.ClassName, self.ClassID, Function, routines.utils.oneLineSerialize( Arguments ) ) )
  else
    env.info( string.format( "%1s:%20s%05d(%s)" , "I", self.ClassName, self.ClassID, routines.utils.oneLineSerialize( Arguments ) ) )
  end
  
end



--- old stuff

--function BASE:_Destructor()
--  --self:E("_Destructor")
--
--  --self:EventRemoveAll()
--end


-- THIS IS WHY WE NEED LUA 5.2 ...
--function BASE:_SetDestructor()
--
--  -- TODO: Okay, this is really technical...
--  -- When you set a proxy to a table to catch __gc, weak tables don't behave like weak...
--  -- Therefore, I am parking this logic until I've properly discussed all this with the community.
--
--  local proxy = newproxy(true)
--  local proxyMeta = getmetatable(proxy)
--
--  proxyMeta.__gc = function ()
--    env.info("In __gc for " .. self:GetClassNameAndID() )
--    if self._Destructor then
--        self:_Destructor()
--    end
--  end
--
--  -- keep the userdata from newproxy reachable until the object
--  -- table is about to be garbage-collected - then the __gc hook
--  -- will be invoked and the destructor called
--  rawset( self, '__proxy', proxy )
--  
--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
  -- @extends Core.Base#BASE


  --- Management of DCS User Flags.
  -- 
  -- # 1. USERFLAG constructor
  --   
  --   * @{#USERFLAG.New}(): Creates a new USERFLAG object.
  -- 
  -- @field #USERFLAG
  USERFLAG = {
    ClassName = "USERFLAG",
  }
  
  --- 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


  --- Set the userflag to a given Number.
  -- @param #USERFLAG self
  -- @param #number Number The number value to be checked if it is the same as the userflag.
  -- @return #USERFLAG The userflag instance.
  -- @usage
  --   local BlueVictory = USERFLAG:New( "VictoryBlue" )
  --   BlueVictory:Set( 100 ) -- Set the UserFlag VictoryBlue to 100.
  --   
  function USERFLAG:Set( Number ) --R2.3
  
    trigger.action.setUserFlag( self.UserFlagName, Number )
    
    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( Number ) --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** - Manage user sound.
--
-- ===
-- 
-- ## Features:
-- 
--   * Play sounds wihtin running missions.
-- 
-- ===
-- 
-- Management of DCS User Sound.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module Core.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 ) --R2.3
  
    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 ) --R2.3
    
    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() --R2.3
    
    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 ) --R2.3
    
    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 ) --R2.3
    
    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.
  -- @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 sound that Blue has won to the player group.
  --   
  function USERSOUND:ToGroup( Group ) --R2.3
    
    trigger.action.outSoundForGroup( Group:GetID(), self.UserSoundFileName )
    
    return self
  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.
-- @param #REPORT self
-- @param #string Text
-- @return #REPORT
function REPORT:AddIndent( Text, Separator ) --R2.1
  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 delimeter, 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 source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SCH%20-%20Scheduler)
-- 
-- ### [SCHEDULER Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SCH%20-%20Scheduler)
--
-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases)
-- 
-- ===
-- 
-- # YouTube Channel
-- 
-- ### [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 #number ScheduleID the ID of the scheduler.
-- @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.
-- 
--     SchedulerObject = SCHEDULER:New()
--     SchedulerID = SchedulerObject:Schedule( nil, ScheduleFunction, {} )
-- 
-- The above example creates a new SchedulerObject, but does not schedule anything.
-- A separate schedule is created by using the SchedulerObject 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 nillified or destroyed, the SCHEDULER object will also be destroyed and stopped after garbage collection.
-- 
--     ZoneObject = ZONE:New( "ZoneName" )
--     SchedulerObject = SCHEDULER:New( ZoneObject )
--     SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} )
--     ...
--     ZoneObject = nil
--     garbagecollect()
-- 
-- The above example creates a new SchedulerObject, 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 SchedulerObject 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 ScheduleObject 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.
--   
--     SchedulerObject, SchedulerID = SCHEDULER:New( nil, ScheduleFunction, {} )
--     
-- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call.
-- Note that 2 variables are returned here: SchedulerObject, 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" )
--     SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} )
--     SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} )
--     ...
--     ZoneObject = nil
--     garbagecollect()
--     
-- The above example creates a new SchedulerObject, and schedules a method call (ScheduleFunction), 
-- and is bound to the existence of ZoneObject, which is a ZONE object (ZoneObject).
-- Both a ScheduleObject and a SchedulerID variable are returned.
-- Later in the logic, the ZoneObject is put to nil, and garbage is collected.
-- As a result, the ScheduleObject 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" )
--     SchedulerObject, SchedulerID = SCHEDULER:New( ZoneObject, ScheduleFunction, {} )
--     SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {}, 10, 10 )
--     ...
--     SchedulerObject:Stop( SchedulerID )
--     ...
--     SchedulerObject:Start( SchedulerID )
--     
-- The above example creates a new SchedulerObject, and does schedule the first schedule as part of the call.
-- Note that 2 variables are returned here: SchedulerObject, 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" )
--     SchedulerObject = SCHEDULER:New( ZoneObject )
-- 
-- Several parameters can be specified that influence the behaviour of a Schedule.
-- 
-- ### A single schedule, immediately executed
-- 
--     SchedulerID = SchedulerObject:Schedule( ZoneObject, ScheduleFunction, {} )
-- 
-- The above example schedules a new ScheduleFunction call to be executed asynchronously, within milleseconds ...
-- 
-- ### A single schedule, planned over time
-- 
--     SchedulerID = SchedulerObject: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 = SchedulerObject: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 = SchedulerObject: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 = SchedulerObject: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 = {},
}

--- SCHEDULER constructor.
-- @param #SCHEDULER self
-- @param #table SchedulerObject 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 #number The ScheduleID of the planned schedule.
function SCHEDULER:New( SchedulerObject, 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 = SchedulerObject
  
  if SchedulerFunction then
    ScheduleID = self:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop )
  end

  return self, ScheduleID
end

--function SCHEDULER:_Destructor()
--  --self:E("_Destructor")
--
--  _SCHEDULEDISPATCHER:RemoveSchedule( self.CallID )
--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 SchedulerObject 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 #number The ScheduleID of the planned schedule.
function SCHEDULER:Schedule( SchedulerObject, SchedulerFunction, SchedulerArguments, Start, Repeat, RandomizeFactor, Stop )
  self:F2( { Start, Repeat, RandomizeFactor, Stop } )
  self:T3( { SchedulerArguments } )

  local ObjectName = "-"
  if SchedulerObject and SchedulerObject.ClassName and SchedulerObject.ClassID then 
    ObjectName = SchedulerObject.ClassName .. SchedulerObject.ClassID
  end
  self:F3( { "Schedule :", ObjectName, tostring( SchedulerObject ),  Start, Repeat, RandomizeFactor, Stop } )
  self.SchedulerObject = SchedulerObject
  
  local ScheduleID = _SCHEDULEDISPATCHER:AddSchedule( 
    self, 
    SchedulerFunction,
    SchedulerArguments,
    Start,
    Repeat,
    RandomizeFactor,
    Stop
  )
  
  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 #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule.
function SCHEDULER:Start( ScheduleID )
  self:F3( { ScheduleID } )

  _SCHEDULEDISPATCHER:Start( self, ScheduleID )
end

--- Stops the schedules or a specific schedule if a valid ScheduleID is provided.
-- @param #SCHEDULER self
-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule.
function SCHEDULER:Stop( ScheduleID )
  self:F3( { ScheduleID } )

  _SCHEDULEDISPATCHER:Stop( self, ScheduleID )
end

--- Removes a specific schedule if a valid ScheduleID is provided.
-- @param #SCHEDULER self
-- @param #number ScheduleID (optional) The ScheduleID of the planned (repeating) schedule.
function SCHEDULER:Remove( ScheduleID )
  self:F3( { ScheduleID } )

  _SCHEDULEDISPATCHER:Remove( self, ScheduleID )
end

--- Clears all pending schedules.
-- @param #SCHEDULER self
function SCHEDULER:Clear()
  self:F3( )

  _SCHEDULEDISPATCHER:Clear( 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 thorought 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 none persistency of SCHEDULERS attached to objects is required to allow SCHEDULER objects to be garbage collectged, when the parent object is also desroyed or nillified 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

--- The SCHEDULEDISPATCHER structure
-- @type SCHEDULEDISPATCHER
SCHEDULEDISPATCHER = {
  ClassName = "SCHEDULEDISPATCHER",
  CallID = 0,
}

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 nillified.
-- Nothing of this code should be modified without testing it thoroughly.
-- @param #SCHEDULEDISPATCHER self
-- @param Core.Scheduler#SCHEDULER Scheduler
function SCHEDULEDISPATCHER:AddSchedule( Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop )
  self:F2( { Scheduler, ScheduleFunction, ScheduleArguments, Start, Repeat, Randomize, Stop } )

  self.CallID = self.CallID + 1
  local CallID = self.CallID .. "#" .. ( Scheduler.MasterObject and Scheduler.MasterObject.GetClassNameAndID and Scheduler.MasterObject:GetClassNameAndID() or "" ) 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.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
    self.ObjectSchedulers[CallID] = Scheduler
    self:F3( { CallID = CallID, ObjectScheduler = tostring(self.ObjectSchedulers[CallID]), MasterObject = tostring(Scheduler.MasterObject) } )
  else
    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] = {}
  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 + .1
  self.Schedule[Scheduler][CallID].Repeat = Repeat or 0
  self.Schedule[Scheduler][CallID].Randomize = Randomize or 0
  self.Schedule[Scheduler][CallID].Stop = Stop

  self:T3( self.Schedule[Scheduler][CallID] )

  self.Schedule[Scheduler][CallID].CallHandler = function( CallID )
    --self:E( CallID )

    local ErrorHandler = function( errmsg )
      env.info( "Error in timer function: " .. errmsg )
      if BASE.Debug ~= nil then
        env.info( BASE.Debug.traceback() )
      end
      return errmsg
    end
    
    local Scheduler = self.ObjectSchedulers[CallID]
    if not Scheduler then
      Scheduler = self.PersistentSchedulers[CallID]
    end
    
    --self:T3( { Scheduler = Scheduler } )
    
    if Scheduler then

      local MasterObject = tostring(Scheduler.MasterObject) 
      local Schedule = self.Schedule[Scheduler][CallID]
      
      --self:T3( { Schedule = Schedule } )

      local SchedulerObject = Scheduler.SchedulerObject
      --local ScheduleObjectName = Scheduler.SchedulerObject:GetNameAndClassID()
      local ScheduleFunction = Schedule.Function
      local ScheduleArguments = Schedule.Arguments
      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 Status, Result
      --self:E( { SchedulerObject = SchedulerObject } )
      if SchedulerObject then
        local function Timer()
          return ScheduleFunction( SchedulerObject, unpack( ScheduleArguments ) ) 
        end
        Status, Result = xpcall( Timer, ErrorHandler )
      else
        local function Timer()
          return ScheduleFunction( unpack( ScheduleArguments ) ) 
        end
        Status, Result = xpcall( Timer, ErrorHandler )
      end
      
      local CurrentTime = timer.getTime()
      local StartTime = Schedule.StartTime

      self:F3( { 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.01
          --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:E( "Scheduled obsolete call for CallID: " .. CallID )
    end
    
    return nil
  end
  
  self:Start( Scheduler, CallID )
  
  return CallID
end

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

function SCHEDULEDISPATCHER:Start( Scheduler, CallID )
  self:F2( { Start = CallID, Scheduler = Scheduler } )

  if CallID then
    local Schedule = self.Schedule[Scheduler]
    -- Only start when there is no ScheduleID defined!
    -- This prevents to "Start" the scheduler twice with the same CallID...
    if not Schedule[CallID].ScheduleID then
      Schedule[CallID].StartTime = timer.getTime()  -- Set the StartTime field to indicate when the scheduler started.
      Schedule[CallID].ScheduleID = timer.scheduleFunction( 
        Schedule[CallID].CallHandler, 
        CallID, 
        timer.getTime() + Schedule[CallID].Start 
      )
    end
  else
    for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
      self:Start( Scheduler, CallID ) -- Recursive
    end
  end
end

function SCHEDULEDISPATCHER:Stop( Scheduler, CallID )
  self:F2( { Stop = CallID, Scheduler = Scheduler } )

  if CallID then
    local Schedule = self.Schedule[Scheduler]
    -- Only stop when there is a ScheduleID defined for the CallID.
    -- So, when the scheduler was stopped before, do nothing.
    if Schedule[CallID].ScheduleID then
      timer.removeFunction( Schedule[CallID].ScheduleID )
      Schedule[CallID].ScheduleID = nil
    end
  else
    for CallID, Schedule in pairs( self.Schedule[Scheduler] or {} ) do
      self:Stop( Scheduler, CallID ) -- Recursive
    end
  end
end

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



--- **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 occuring 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 levels of kind 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: Subsets of the _DATABASE object. 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 teh 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 @{BASE} class, @{UNIT} class and @{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_REMOVE_UNIT = world.event.S_EVENT_MAX + 1004


--- 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,
  MarkAdded =         world.event.S_EVENT_MARK_ADDED,
  MarkChange =        world.event.S_EVENT_MARK_CHANGE,
  MarkRemoved =       world.event.S_EVENT_MARK_REMOVED,
  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,
  RemoveUnit =        world.event.S_EVENT_REMOVE_UNIT,
}

--- 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 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 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 weapon The weapon used during the event.
-- @field Weapon
-- @field WeaponName
-- @field WeaponTgtDCSUnit



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.RemoveUnit] = {
     Order = -1,
     Event = "OnEventRemoveUnit",
     Text = "S_EVENT_REMOVE_UNIT" 
   },
}


--- The Events structure
-- @type EVENT.Events
-- @field #number IniUnit

function EVENT:New()
  local self = BASE:Inherit( self, BASE:New() )
  self:F2()
  self.EventHandler = world.addEventHandler( self )
  return self
end


--- Initializes the Events structure for the event
-- @param #EVENT self
-- @param DCS#world.event EventID
-- @param Core.Base#BASE EventClass
-- @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
-- @return #EVENT.Events
function EVENT:RemoveEvent( EventClass, EventID  )

  self:F2( { "Removing subscription for class: ", EventClass:GetClassNameAndID() } )

  local EventPriority = EventClass:GetEventPriority()

  self.Events = self.Events or {}
  self.Events[EventID] = self.Events[EventID] or {}
  self.Events[EventID][EventPriority] = self.Events[EventID][EventPriority] or {}  
    
  self.Events[EventID][EventPriority][EventClass] = nil
  
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
-- @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 EventObject
function EVENT:RemoveAll( EventObject  )
  self:F3( { EventObject:GetClassNameAndID() } )

  local EventClass = EventObject:GetClassNameAndID()
  local EventPriority = EventClass:GetEventPriority()
  for EventID, EventData in pairs( self.Events ) do
    self.Events[EventID][EventPriority][EventClass] = nil
  end
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
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 } )

  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
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 EventID
-- @return #EVENT
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
  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
  -- @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: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 EventClass The self instance of the class for which the event is.
  -- @return #EVENT
  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
  -- @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: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:I( { 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 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

end

--- @param #EVENT self
-- @param #EVENTDATA Event
function EVENT:onEvent( Event )

  local ErrorHandler = function( errmsg )

    env.info( "Error in SCHEDULER function:" .. errmsg )
    if BASE.Debug ~= nil then
      env.info( debug.traceback() )
    end
    
    return errmsg
  end


  local EventMeta = _EVENTMETA[Event.id]

  --self:E( { EventMeta.Text, Event } )  -- Activate the see all incoming events ...

  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

    if Event.id and Event.id == EVENTS.MissionEnd then
      self.MissionEnd = true
    end
    
    if Event.initiator then    
      
      Event.IniObjectCategory = Event.initiator:getCategory()

      if Event.IniObjectCategory == Object.Category.UNIT then
        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 = ""
        if Event.IniDCSGroup and Event.IniDCSGroup:isExist() then
          Event.IniDCSGroupName = Event.IniDCSGroup:getName()
          Event.IniGroup = GROUP:FindByName( Event.IniDCSGroupName )
          if Event.IniGroup then
            Event.IniGroupName = Event.IniDCSGroupName
          end
        end
        Event.IniPlayerName = Event.IniDCSUnit:getPlayerName()
        Event.IniCoalition = Event.IniDCSUnit:getCoalition()
        Event.IniTypeName = Event.IniDCSUnit:getTypeName()
        Event.IniCategory = Event.IniDCSUnit:getDesc().category
      end
      
      if Event.IniObjectCategory == Object.Category.STATIC then
        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

      if Event.IniObjectCategory == Object.Category.CARGO then
        Event.IniDCSUnit = Event.initiator
        Event.IniDCSUnitName = Event.IniDCSUnit:getName()
        Event.IniUnitName = Event.IniDCSUnitName
        Event.IniUnit = CARGO:FindByName( Event.IniDCSUnitName )
        Event.IniCoalition = Event.IniDCSUnit:getCoalition()
        Event.IniCategory = Event.IniDCSUnit:getDesc().category
        Event.IniTypeName = Event.IniDCSUnit:getTypeName()
      end

      if Event.IniObjectCategory == Object.Category.SCENERY then
        Event.IniDCSUnit = Event.initiator
        Event.IniDCSUnitName = Event.IniDCSUnit:getName()
        Event.IniUnitName = Event.IniDCSUnitName
        Event.IniUnit = SCENERY:Register( Event.IniDCSUnitName, Event.initiator )
        Event.IniCategory = Event.IniDCSUnit:getDesc().category
        Event.IniTypeName = Event.initiator:isExist() and Event.IniDCSUnit:getTypeName() or "SCENERY" -- TODO: Bug fix for 2.1!
      end
    end
    
    if Event.target then

      Event.TgtObjectCategory = Event.target:getCategory()

      if Event.TgtObjectCategory == Object.Category.UNIT then 
        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 )
          if Event.TgtGroup then
            Event.TgtGroupName = Event.TgtDCSGroupName
          end
        end
        Event.TgtPlayerName = Event.TgtDCSUnit:getPlayerName()
        Event.TgtCoalition = Event.TgtDCSUnit:getCoalition()
        Event.TgtCategory = Event.TgtDCSUnit:getDesc().category
        Event.TgtTypeName = Event.TgtDCSUnit:getTypeName()
      end
      
      if Event.TgtObjectCategory == Object.Category.STATIC then
        Event.TgtDCSUnit = Event.target
        Event.TgtDCSUnitName = Event.TgtDCSUnit:getName()
        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

      if Event.TgtObjectCategory == Object.Category.SCENERY then
        Event.TgtDCSUnit = Event.target
        Event.TgtDCSUnitName = Event.TgtDCSUnit:getName()
        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
    
    if Event.weapon then
      Event.Weapon = Event.weapon
      Event.WeaponName = Event.Weapon:getTypeName()
      Event.WeaponUNIT = CLIENT:Find( Event.Weapon, '', true ) -- Sometimes, the weapon is a player unit!
      Event.WeaponPlayerName = Event.WeaponUNIT and Event.Weapon:getPlayerName()
      Event.WeaponCoalition = Event.WeaponUNIT and Event.Weapon:getCoalition()
      Event.WeaponCategory = Event.WeaponUNIT and Event.Weapon:getDesc().category
      Event.WeaponTypeName = Event.WeaponUNIT 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      
      Event.Place=AIRBASE:Find(Event.place)
      Event.PlaceName=Event.Place:GetName()
    end

--  @FC: something like this should be added.
--[[    
    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.MarkGroupID = Event.groupID
    end
]]
    
    if Event.cargo then
      Event.Cargo = Event.cargo
      Event.CargoName = Event.cargo.Name
    end

    if Event.zone then
      Event.Zone = Event.zone
      Event.ZoneName = Event.zone.ZoneName
    end
    
    local PriorityOrder = EventMeta.Order
    local PriorityBegin = PriorityOrder == -1 and 5 or 1
    local PriorityEnd = PriorityOrder == -1 and 1 or 5

    if Event.IniObjectCategory ~= Object.Category.STATIC then
      self:T( { EventMeta.Text, Event, Event.IniDCSUnitName, Event.TgtDCSUnitName, PriorityOrder } )
    end
    
    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 = GROUP:FindByName( Event.IniDCSGroupName )
          Event.TgtGroup = 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 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
              
                  if Event.IniObjectCategory ~= 3 then
                    self:F( { "Calling EventFunction for UNIT ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } )
                  end
                                  
                  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.
                    if Event.IniObjectCategory ~= 3 then
                      self:F( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } )
                    end
                                  
                    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 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
    
                    if Event.IniObjectCategory ~= 3 then
                      self:F( { "Calling EventFunction for GROUP ", EventClass:GetClassNameAndID(), ", Unit ", Event.IniUnitName, EventPriority } )
                    end
                                      
                    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.
                      if Event.IniObjectCategory ~= 3 then
                        self:F( { "Calling " .. EventMeta.Event .. " for GROUP ", EventClass:GetClassNameAndID(), EventPriority } )
                      end
                                          
                      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.
                  if Event.IniObjectCategory ~= 3 then
                    self:F2( { "Calling EventFunction for Class ", EventClass:GetClassNameAndID(), EventPriority } )
                  end                
                  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.
                    if Event.IniObjectCategory ~= 3 then
                      self:F2( { "Calling " .. EventMeta.Event .. " for Class ", EventClass:GetClassNameAndID(), EventPriority } )
                    end
                                  
                    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
  
  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
function EVENTHANDLER:New()
  self = BASE:Inherit( self, BASE:New() ) -- #EVENTHANDLER
  return self
end
--- **Core** - Manages various settings for running missions, consumed by moose classes and provides a menu system 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 behaviour of certain functionalities and classes within the MOOSE framework.
-- 
-- ===
-- 
-- The SETTINGS class takes care of various settings that influence the behaviour 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 choosen 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 requried 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: Lattitude Longitude [Degrees Minutes Seconds](https://en.wikipedia.org/wiki/Geographic_coordinate_conversion). The accuracy can also be adapted.
--   - A2G LL DDM: Lattitude 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 choosen.
-- 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 @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. 
--   - @{#SETTINGS.GetMessageTime}(): Retrieves for a specific @{Message.MESSAGE.MessageType} the duration to be displayed in seconds. 
--   
-- ===
-- 
-- @field #SETTINGS
SETTINGS = {
  ClassName = "SETTINGS",
  ShowPlayerMenu = true,
}



do -- SETTINGS

  --- SETTINGS constructor.
  -- @param #SETTINGS self
  -- @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 )
      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
  
 
  --- Sets the SETTINGS metric.
  -- @param #SETTINGS self
  function SETTINGS:SetMetric()
    self.Metric = true
  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:IsMetric() )
  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
  -- @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
  -- @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 )

    local A2GCoordinateMenu = MENU_GROUP:New( MenuGroup, "A2G Coordinate System", SettingsMenu ):SetTime( MenuTime )
  
    
    if not self:IsA2G_LL_DMS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime )
    end
    
    if not self:IsA2G_LL_DDM() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime )
    end
    
    if self:IsA2G_LL_DDM() then
      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
    
    if not self:IsA2G_BR() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "BR" ):SetTime( MenuTime )
    end
    
    if not self:IsA2G_MGRS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.A2GMenuSystem, self, MenuGroup, RootMenu, "MGRS" ):SetTime( MenuTime )
    end
    
    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

    local A2ACoordinateMenu = MENU_GROUP:New( MenuGroup, "A2A Coordinate System", SettingsMenu ):SetTime( MenuTime )

    if not self:IsA2A_LL_DMS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DMS" ):SetTime( MenuTime )
    end

    if not self:IsA2A_LL_DDM() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "LL DDM" ):SetTime( MenuTime )
    end

    if self:IsA2A_LL_DDM() then
      MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 1", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 1 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 2", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 2 ):SetTime( MenuTime )
      MENU_GROUP_COMMAND:New( MenuGroup, "LL DDM Accuracy 3", A2ACoordinateMenu, self.MenuLL_DDM_Accuracy, self, MenuGroup, RootMenu, 3 ):SetTime( MenuTime )
    end    

    if not self:IsA2A_BULLS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BULLS" ):SetTime( MenuTime )
    end
    
    if not self:IsA2A_BRAA() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.A2AMenuSystem, self, MenuGroup, RootMenu, "BRAA" ):SetTime( MenuTime )
    end
    
    if not self:IsA2A_MGRS() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Military Grid (MGRS)", 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 MetricsMenu = MENU_GROUP:New( MenuGroup, "Measures and Weights System", SettingsMenu ):SetTime( MenuTime )
    
    if self:IsMetric() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, false ):SetTime( MenuTime )
    end
    
    if self:IsImperial() then
      MENU_GROUP_COMMAND:New( MenuGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuMWSystem, self, MenuGroup, RootMenu, true ):SetTime( MenuTime )
    end    
    
    local MessagesMenu = MENU_GROUP:New( MenuGroup, "Messages and Reports", 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()
      local PlayerNames = PlayerGroup:GetPlayerNames()
  
      local PlayerMenu = MENU_GROUP:New( PlayerGroup, 'Settings "' .. PlayerName .. '"' )
      
      self.PlayerMenu = PlayerMenu
  
      local A2GCoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2G Coordinate System", PlayerMenu )
    
      if not self:IsA2G_LL_DMS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" )
      end
    
      if not self:IsA2G_LL_DDM() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" )
      end
    
      if self:IsA2G_LL_DDM() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 )
      end
      
      if not self:IsA2G_BR() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing, Range (BR)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "BR" )
      end
      
      if not self:IsA2G_MGRS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" )
      end    
  
      if self:IsA2G_MGRS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 1", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 2", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 3", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 4", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "MGRS Accuracy 5", A2GCoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 )
      end
  
      local A2ACoordinateMenu = MENU_GROUP:New( PlayerGroup, "A2A Coordinate System", PlayerMenu )
  
  
      if not self:IsA2A_LL_DMS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Min Sec (LL DMS)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DMS" )
      end
    
      if not self:IsA2A_LL_DDM() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Lat/Lon Degree Dec Min (LL DDM)", A2GCoordinateMenu, self.MenuGroupA2GSystem, self, PlayerUnit, PlayerGroup, PlayerName, "LL DDM" )
      end
    
      if self:IsA2A_LL_DDM() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 1", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 2", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "LL DDM Accuracy 3", A2GCoordinateMenu, self.MenuGroupLL_DDM_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 )
      end
  
      if not self:IsA2A_BULLS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Bullseye (BULLS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BULLS" )
      end
      
      if not self:IsA2A_BRAA() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Bearing Range Altitude Aspect (BRAA)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "BRAA" )
      end
      
      if not self:IsA2A_MGRS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS)", A2ACoordinateMenu, self.MenuGroupA2ASystem, self, PlayerUnit, PlayerGroup, PlayerName, "MGRS" )
      end
      
      if self:IsA2A_MGRS() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 1", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 1 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 2", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 2 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 3", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 3 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 4", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 4 )
        MENU_GROUP_COMMAND:New( PlayerGroup, "Military Grid (MGRS) Accuracy 5", A2ACoordinateMenu, self.MenuGroupMGRS_AccuracySystem, self, PlayerUnit, PlayerGroup, PlayerName, 5 )
      end    
  
      local MetricsMenu = MENU_GROUP:New( PlayerGroup, "Measures and Weights System", PlayerMenu )
      
      if self:IsMetric() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Imperial (Miles,Feet)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, false )
      end
      
      if self:IsImperial() then
        MENU_GROUP_COMMAND:New( PlayerGroup, "Metric (Kilometers,Meters)", MetricsMenu, self.MenuGroupMWSystem, self, PlayerUnit, PlayerGroup, PlayerName, true )
      end    
  
  
      local MessagesMenu = MENU_GROUP:New( PlayerGroup, "Messages and Reports", PlayerMenu )
  
      local UpdateMessagesMenu = MENU_GROUP:New( PlayerGroup, "Update Messages", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "Off", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 0 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 5 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 10 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", UpdateMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Update, 60 )
  
      local InformationMessagesMenu = MENU_GROUP:New( PlayerGroup, "Information Messages", MessagesMenu )
      MENU_GROUP_COMMAND:New( PlayerGroup, "5 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 5 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "10 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 10 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "15 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", InformationMessagesMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Information, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", 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, "15 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", BriefingReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Briefing, 120 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", 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, "15 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", OverviewReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.Overview, 120 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", 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, "15 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 15 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "30 seconds", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 30 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "1 minute", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 60 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "2 minutes", DetailedReportsMenu, self.MenuGroupMessageTimingsSystem, self, PlayerUnit, PlayerGroup, PlayerName, MESSAGE.Type.DetailedReportsMenu, 120 )
      MENU_GROUP_COMMAND:New( PlayerGroup, "3 minutes", 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( {self, PlayerUnit:GetName(), A2GSystem} )
      self.A2GSystem = A2GSystem
      MESSAGE:New( string.format( "Settings: A2G format set to %s for player %s.", A2GSystem, PlayerName ), 5 ):ToGroup( PlayerGroup )
      self:RemovePlayerMenu(PlayerUnit)
      self:SetPlayerMenu(PlayerUnit)
    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 )
      self:RemovePlayerMenu(PlayerUnit)
      self:SetPlayerMenu(PlayerUnit)
    end
  
    --- @param #SETTINGS self
    function SETTINGS:MenuGroupLL_DDM_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, LL_Accuracy )
      self.LL_Accuracy = LL_Accuracy
      MESSAGE:New( string.format( "Settings: A2G LL format accuracy set to %d for player %s.", LL_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup )
      self:RemovePlayerMenu(PlayerUnit)
      self:SetPlayerMenu(PlayerUnit)
    end
  
    --- @param #SETTINGS self
    function SETTINGS:MenuGroupMGRS_AccuracySystem( PlayerUnit, PlayerGroup, PlayerName, MGRS_Accuracy )
      self.MGRS_Accuracy = MGRS_Accuracy
      MESSAGE:New( string.format( "Settings: A2G MGRS format accuracy set to %d for player %s.", MGRS_Accuracy, PlayerName ), 5 ):ToGroup( PlayerGroup )
      self:RemovePlayerMenu(PlayerUnit)
      self:SetPlayerMenu(PlayerUnit)
    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 )
      self:RemovePlayerMenu(PlayerUnit)
      self:SetPlayerMenu(PlayerUnit)
    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

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 obscolete 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 scanerios 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.
-- 
-- ===
--- 
-- ### 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()
    return self.Group[MenuGroupName].Menus[Path]
  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 

end








do -- MENU_BASE

  --- @type MENU_BASE
  -- @extends 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
  }
  
  --- Consructor
  -- @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.MenuTime = 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 time stamp for later prevention of menu removal.
  -- @param #MENU_BASE self
  -- @param MenuTime
  -- @return #MENU_BASE
  function MENU_BASE:SetTime( MenuTime )
    self.MenuTime = MenuTime
    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 perent 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

  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( MenuTime, 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 MenuTime or self.MenuTime ~= MenuTime 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

  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 MenuTime or self.MenuTime ~= MenuTime 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}s.  
  -- 
  -- 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 perent 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

  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( MenuTime, 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 MenuTime or self.MenuTime ~= MenuTime 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

  end
  
  --- Removes a radio command item for a coalition
  -- @param #MENU_COALITION_COMMAND self
  -- @return #nil
  function MENU_COALITION_COMMAND:Remove( MenuTime, 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 MenuTime or self.MenuTime ~= MenuTime 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 dissapear 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

  end
  
  --- Removes the sub menus recursively of this MENU_GROUP.
  -- @param #MENU_GROUP self
  -- @param MenuTime
  -- @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( MenuTime, MenuTag )

    for MenuText, Menu in pairs( self.Menus or {} ) do
      Menu:Remove( MenuTime, MenuTag )
    end
    
    self.Menus = nil
  
  end


  --- Removes the main menu and sub menus recursively of this MENU_GROUP.
  -- @param #MENU_GROUP self
  -- @param MenuTime
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP:Remove( MenuTime, 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( MenuTime, MenuTag )
      if not MenuTime or self.MenuTime ~= MenuTime 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

  end
  
  --- Removes a menu structure for a group.
  -- @param #MENU_GROUP_COMMAND self
  -- @param MenuTime
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP_COMMAND:Remove( MenuTime, 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 MenuTime or self.MenuTime ~= MenuTime 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()

    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

  end
  
  --- Removes the sub menus recursively of this MENU_GROUP_DELAYED.
  -- @param #MENU_GROUP_DELAYED self
  -- @param MenuTime
  -- @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( MenuTime, MenuTag )

    for MenuText, Menu in pairs( self.Menus or {} ) do
      Menu:Remove( MenuTime, MenuTag )
    end
    
    self.Menus = nil
  
  end


  --- Removes the main menu and sub menus recursively of this MENU_GROUP.
  -- @param #MENU_GROUP_DELAYED self
  -- @param MenuTime
  -- @param MenuTag A Tag or Key to filter the menus to be refreshed with the Tag set.
  -- @return #nil
  function MENU_GROUP_DELAYED:Remove( MenuTime, 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( MenuTime, MenuTag )
      if not MenuTime or self.MenuTime ~= MenuTime 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

  end
  
  --- Removes a menu structure for a group.
  -- @param #MENU_GROUP_COMMAND_DELAYED self
  -- @param MenuTime
  -- @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( MenuTime, 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 MenuTime or self.MenuTime ~= MenuTime 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 behaviour. Some zones are static, while others are moveable.
--   * Enquiry 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.
--   
-- 
-- There are essentially two core functions that zones accomodate:
-- 
--   * Test if an object is within the zone boundaries.
--   * Provide the zone behaviour. 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.
--
-- === 
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @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.
-- @extends Core.Base#BASE


--- 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 @{Core.Zone#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.
-- 
-- @field #ZONE_BASE
ZONE_BASE = {
  ClassName = "ZONE_BASE",
  ZoneName = "",
  ZoneProbability = 1,
  }


--- 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, BASE:New() )
  self:F( ZoneName )

  self.ZoneName = ZoneName
  
  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 )
  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 )
  local InZone = self:IsVec2InZone( Coordinate:GetVec2() )
  return InZone
end

--- Returns if a PointVec2 is within the zone.
-- @param #ZONE_BASE self
-- @param Core.Point#POINT_VEC2 PointVec2 The PointVec2 to test.
-- @return #boolean true if the PointVec2 is within the zone.
function ZONE_BASE:IsPointVec2InZone( PointVec2 )
  local InZone = self:IsVec2InZone( PointVec2:GetVec2() )
  return InZone
end

--- Returns if a PointVec3 is within the zone.
-- @param #ZONE_BASE self
-- @param Core.Point#POINT_VEC3 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#POINT_VEC2} 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#POINT_VEC2 The PointVec2 of the zone.
function ZONE_BASE:GetPointVec2()
  self:F2( self.ZoneName )
  
  local Vec2 = self:GetVec2()

  local PointVec2 = POINT_VEC2:NewFromVec2( Vec2 )

  self:T2( { PointVec2 } )
  
  return PointVec2  
end


--- Returns a @{Core.Point#COORDINATE} of the zone.
-- @param #ZONE_BASE self
-- @return Core.Point#COORDINATE The Coordinate of the zone.
function ZONE_BASE:GetCoordinate()
  self:F2( self.ZoneName )
  
  local Vec2 = self:GetVec2()

  local Coordinate = COORDINATE:NewFromVec2( Vec2 )

  self:T2( { Coordinate } )
  
  return Coordinate  
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#POINT_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 Core.Point#POINT_VEC3 The PointVec3 of the zone.
function ZONE_BASE:GetPointVec3( Height )
  self:F2( self.ZoneName )
  
  local Vec3 = self:GetVec3( Height )

  local PointVec3 = POINT_VEC3: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 )

  local PointVec3 = COORDINATE:NewFromVec3( Vec3 )

  self:T2( { PointVec3 } )
  
  return PointVec3  
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#POINT_VEC2} within the zone.
-- @param #ZONE_BASE self
-- @return Core.Point#POINT_VEC2 The PointVec2 coordinates.
function ZONE_BASE:GetRandomPointVec2()
  return nil
end

--- Define a random @{Core.Point#POINT_VEC3} within the zone.
-- @param #ZONE_BASE self
-- @return Core.Point#POINT_VEC3 The PointVec3 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 { x1 = 0, y1 = 0, x2 = 0, y2 = 0 }
  return nil
end

--- Bound the zone boundaries with a tires.
-- @param #ZONE_BASE self
function ZONE_BASE:BoundZone()
  self:F2()

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 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


--- 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 Core.Zone#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#POINT_VEC2} object representing a random 2D point in the zone.
--   * @{#ZONE_RADIUS.GetRandomPointVec3}(): Gets a @{Core.Point#POINT_VEC3} object representing a random 3D point in the zone. Note that the height of the point is at landheight.
-- 
-- @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.
-- @return #ZONE_RADIUS self
function ZONE_RADIUS:New( ZoneName, Vec2, Radius )
	local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) ) -- #ZONE_RADIUS
	self:F( { ZoneName, Vec2, Radius } )

	self.Radius = Radius
	self.Vec2 = Vec2
	
	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()

  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()
    POINT_VEC2:New( Point.x, Point.y, AddHeight ):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()
    POINT_VEC2:New( Point.x, Point.y, AddHeight ):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 after a zone has been scanned, the zone can be evaluated by:
-- 
--   * @{ZONE_RADIUS.IsAllInZoneOfCoalition}(): Scan the presence of units in the zone of a coalition.
--   * @{ZONE_RADIUS.IsAllInZoneOfOtherCoalition}(): Scan the presence of units in the zone of an other coalition.
--   * @{ZONE_RADIUS.IsSomeInZoneOfCoalition}(): Scan if there is some presence of units in the zone of the given coalition.
--   * @{ZONE_RADIUS.IsNoneInZoneOfCoalition}(): Scan if there isn't any presence of units in the zone of an other coalition than the given one.
--   * @{ZONE_RADIUS.IsNoneInZone}(): Scan if the zone is empty.
-- @{#ZONE_RADIUS.
-- @param #ZONE_RADIUS self
-- @param ObjectCategories
-- @param Coalition
-- @usage
--    self.Zone:Scan()
--    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
function ZONE_RADIUS:Scan( ObjectCategories )

  self.ScanData = {}
  self.ScanData.Coalitions = {}
  self.ScanData.Scenery = {}
  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 then  
      local ObjectCategory = ZoneObject:getCategory()
      if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or 
         (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then
        local CoalitionDCSUnit = ZoneObject:getCoalition()
        self.ScanData.Coalitions[CoalitionDCSUnit] = true
        self.ScanData.Units[ZoneObject] = ZoneObject
        self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } )
      end
      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 )
        self:F2( { SCENERY =  self.ScanData.Scenery[SceneryType][SceneryName] } )
      end
    end
    return true
  end

  world.searchObjects( ObjectCategories, SphereSearch, EvaluateZone )
  
end


function ZONE_RADIUS:GetScannedUnits()

  return self.ScanData.Units
end


function ZONE_RADIUS:CountScannedCoalitions()

  local Count = 0
  
  for CoalitionID, Coalition in pairs( self.ScanData.Coalitions ) do
    Count = Count + 1
  end
  return Count
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 ot 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


function ZONE_RADIUS:GetScannedSceneryType( SceneryType )
  return self.ScanData.Scenery[SceneryType]
end


function ZONE_RADIUS:GetScannedScenery()
  return self.ScanData.Scenery
end


--- Is All in Zone of Coalition?
-- @param #ZONE_RADIUS self
-- @param Coalition
-- @return #boolean
-- @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?
-- 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 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?
-- 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 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 / 2,
      }
    }

  local function EvaluateZone( ZoneDCSUnit )
  
    env.info( ZoneDCSUnit:getName() ) 
  
    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 )
  
  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 )

  local InZone = self:IsVec2InZone( { x = Vec3.x, y = Vec3.z } )

  return InZone
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.
-- @return DCS#Vec2 The random location within the zone.
function ZONE_RADIUS:GetRandomVec2( inner, outer )
	self:F( self.ZoneName, inner, outer )

	local Point = {}
	local Vec2 = self:GetVec2()
	local _inner = inner or 0
	local _outer = outer or self:GetRadius()

	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);
	
	self:T( { Point } )
	
	return Point
end

--- Returns a @{Core.Point#POINT_VEC2} object reflecting a random 2D 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 Core.Point#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone.
function ZONE_RADIUS:GetRandomPointVec2( inner, outer )
  self:F( self.ZoneName, inner, outer )

  local PointVec2 = POINT_VEC2: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#POINT_VEC3} 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. 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#POINT_VEC3 The @{Core.Point#POINT_VEC3} object reflecting the random 3D location within the zone.
function ZONE_RADIUS:GetRandomPointVec3( inner, outer )
  self:F( self.ZoneName, inner, outer )

  local PointVec3 = POINT_VEC3: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. 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
function ZONE_RADIUS:GetRandomCoordinate( inner, outer )
  self:F( self.ZoneName, inner, outer )

  local Coordinate = COORDINATE:NewFromVec2( self:GetRandomVec2(inner, outer) )

  self:T3( { Coordinate = Coordinate } )
  
  return Coordinate
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
function ZONE:New( ZoneName )

  local Zone = trigger.misc.getZone( ZoneName )
  
  if not Zone then
    error( "Zone " .. ZoneName .. " does not exist." )
    return nil
  end

  local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, { x = Zone.point.x, y = Zone.point.z }, Zone.radius ) )
  self:F( ZoneName )

  self.Zone = Zone
  
  return self
end

--- Find a zone in the _DATABASE using the name of the zone.
-- @param #ZONE_BASE self
-- @param #string ZoneName The name of the zone.
-- @return #ZONE_BASE 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#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 Dcs.DCSTypes#Distance Radius The radius of the zone.
-- @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
    
    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
  
  local self = BASE:Inherit( self, ZONE_RADIUS:New( ZoneName, ZoneUNIT:GetVec2(), Radius ) )

  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


--- 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 @{Core.Zone#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 ) )
  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

--- 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#POINT_VEC2} object reflecting a random 2D location within the zone.
-- @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#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone.
function ZONE_GROUP:GetRandomPointVec2( inner, outer )
  self:F( self.ZoneName, inner, outer )

  local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() )

  self:T3( { PointVec2 } )
  
  return PointVec2
end


--- @type ZONE_POLYGON_BASE
-- --@field #ZONE_POLYGON_BASE.ListVec2 Polygon The polygon defined by an array of @{DCS#Vec2}.
-- @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 @{Core.Zone#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#POINT_VEC2} object representing a random 2D point within the zone.
--   * @{#ZONE_POLYGON_BASE.GetRandomPointVec3}(): Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone.
-- 
-- @field #ZONE_POLYGON_BASE
ZONE_POLYGON_BASE = {
  ClassName="ZONE_POLYGON_BASE",
  }

--- A points array.
-- @type ZONE_POLYGON_BASE.ListVec2
-- @list <DCS#Vec2>

--- 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 )
  local self = BASE:Inherit( self, ZONE_BASE:New( ZoneName ) )
  self:F( { ZoneName, PointsArray } )

  local i = 0
  
  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

  return self
end

--- Returns the center location of the polygon.
-- @param #ZONE_GROUP 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

--- 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:E( { Polygon = self.ZoneName, Coordinates = self._.Polygon } )

  return self
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



--- Smokes the zone boundaries in a color.
-- @param #ZONE_POLYGON_BASE self
-- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color.
-- @return #ZONE_POLYGON_BASE self
function ZONE_POLYGON_BASE:SmokeZone( SmokeColor )
  self:F2( SmokeColor )

  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 )
      POINT_VEC2:New( PointX, PointY ):Smoke( SmokeColor )
    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 )

  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

--- Define a random @{DCS#Vec2} within the zone.
-- @param #ZONE_POLYGON_BASE self
-- @return DCS#Vec2 The Vec2 coordinate.
function ZONE_POLYGON_BASE:GetRandomVec2()
  self:F2()

  --- It is a bit tricky to find a random point within a polygon. Right now i am doing it the dirty and inefficient way...
  local Vec2Found = false
  local Vec2
  local BS = self:GetBoundingSquare()
  
  self:T2( BS )
  
  while Vec2Found == false do
    Vec2 = { x = math.random( BS.x1, BS.x2 ), y = math.random( BS.y1, BS.y2 ) }
    self:T2( Vec2 )
    if self:IsVec2InZone( Vec2 ) then
      Vec2Found = true
    end
  end
  
  self:T2( Vec2 )

  return Vec2
end

--- Return a @{Core.Point#POINT_VEC2} object representing a random 2D point at landheight within the zone.
-- @param #ZONE_POLYGON_BASE self
-- @return @{Core.Point#POINT_VEC2}
function ZONE_POLYGON_BASE:GetRandomPointVec2()
  self:F2()

  local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() )
  
  self:T2( PointVec2 )

  return PointVec2
end

--- Return a @{Core.Point#POINT_VEC3} object representing a random 3D point at landheight within the zone.
-- @param #ZONE_POLYGON_BASE self
-- @return @{Core.Point#POINT_VEC3}
function ZONE_POLYGON_BASE:GetRandomPointVec3()
  self:F2()

  local PointVec3 = POINT_VEC3: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


--- @type ZONE_POLYGON
-- @extends #ZONE_POLYGON_BASE


--- The ZONE_POLYGON class defined by a sequence of @{Wrapper.Group#GROUP} waypoints within the Mission Editor, forming a polygon.
-- This class implements the inherited functions from @{Core.Zone#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!!! 
-- 
-- @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 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


--- 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

do -- ZONE_AIRBASE

  --- @type ZONE_AIRBASE
  -- @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 @{Core.Zone#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 ) )
  
    self._.ZoneAirbase = Airbase
    self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2()
  
    -- 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 @{Wrapper.Group}.
  -- @param #ZONE_AIRBASE self
  -- @return DCS#Vec2 The location of the zone based on the @{Wrapper.Group} 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 random location within the zone of the @{Wrapper.Group}.
  -- @param #ZONE_AIRBASE self
  -- @return DCS#Vec2 The random location of the zone based on the @{Wrapper.Group} location.
  function ZONE_AIRBASE:GetRandomVec2()
    self:F( self.ZoneName )
  
    local Point = {}
    local Vec2 = self._.ZoneAirbase: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#POINT_VEC2} object reflecting a random 2D location within the zone.
  -- @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#POINT_VEC2 The @{Core.Point#POINT_VEC2} object reflecting the random 3D location within the zone.
  function ZONE_AIRBASE:GetRandomPointVec2( inner, outer )
    self:F( self.ZoneName, inner, outer )
  
    local PointVec2 = POINT_VEC2:NewFromVec2( self:GetRandomVec2() )
  
    self:T3( { PointVec2 } )
    
    return PointVec2
  end


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.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Core.Database
-- @image Core_Database.JPG


--- @type DATABASE
-- @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
--  
-- On top, for internal MOOSE administration purposes, the DATBASE 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 = {},
}

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 )
  self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.Crash, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.RemoveUnit, self._EventOnDeadOrCrash )
  self:HandleEvent( EVENTS.Hit, self.AccountHits )
  self:HandleEvent( EVENTS.NewCargo )
  self:HandleEvent( EVENTS.DeleteCargo )
  self:HandleEvent( EVENTS.NewZone )
  self:HandleEvent( EVENTS.DeleteZone )
  
  -- Follow alive players and clients
  --self:HandleEvent( EVENTS.PlayerEnterUnit, self._EventOnPlayerEnterUnit ) -- This is not working anymore!, handling this through the birth event.
  self:HandleEvent( EVENTS.PlayerLeaveUnit, self._EventOnPlayerLeaveUnit )
  
  self:_RegisterTemplates()
  self:_RegisterGroupsAndUnits()
  self:_RegisterClients()
  self:_RegisterStatics()
  --self:_RegisterPlayers()
  self:_RegisterAirbases()

  self.UNITS_Position = 0
  
  --- @param #DATABASE self
  local function CheckPlayers( self )
  
    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
      --self:E( { "CoalitionData:", CoalitionData } )
      for UnitId, UnitData in pairs( CoalitionData ) do
        if UnitData and UnitData:isExist() then
        
          local UnitName = UnitData:getName()
          local PlayerName = UnitData:getPlayerName()
          local PlayerUnit = UNIT:Find( UnitData )
          --self:T( { "UnitData:", UnitData, UnitName, PlayerName, PlayerUnit } )

          if PlayerName and PlayerName ~= "" then
            if self.PLAYERS[PlayerName] == nil or self.PLAYERS[PlayerName] ~= UnitName then
              --self:E( { "Add player for unit:", UnitName, PlayerName } )
              self:AddPlayer( UnitName, PlayerName )
              --_EVENTDISPATCHER:CreateEventPlayerEnterUnit( PlayerUnit )
              local Settings = SETTINGS:Set( PlayerName )
              Settings:SetPlayerMenu( PlayerUnit )
            end
          end
        end
      end
    end
  end
  
  --self:E( "Scheduling" )
  --PlayerCheckSchedule = SCHEDULER:New( nil, CheckPlayers, { self }, 1, 1 )
  
  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
function DATABASE:AddUnit( DCSUnitName )

  if not  self.UNITS[DCSUnitName] then
    local UnitRegister = UNIT:Register( DCSUnitName )
    self.UNITS[DCSUnitName] = UNIT:Register( DCSUnitName )
    
    table.insert( self.UNITS_Index, 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.UNITS[DCSUnitName] = nil 
end

--- Adds a Static based on the Static Name in the DATABASE.
-- @param #DATABASE self
function DATABASE:AddStatic( DCSStaticName )

  if not self.STATICS[DCSStaticName] then
    self.STATICS[DCSStaticName] = STATIC:Register( DCSStaticName )
    return self.STATICS[DCSStaticName]
  end
  
  return nil
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 StaticName.
-- @param #DATABASE self
-- @param #string StaticName
-- @return Wrapper.Static#STATIC The found STATIC.
function DATABASE:FindStatic( StaticName )

  local StaticFound = self.STATICS[StaticName]
  return StaticFound
end

--- Finds a 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 Airbase based on the Airbase Name in the DATABASE.
-- @param #DATABASE self
-- @param #string AirbaseName The name of the airbase
function DATABASE:AddAirbase( AirbaseName )

  if not self.AIRBASES[AirbaseName] then
    self.AIRBASES[AirbaseName] = AIRBASE:Register( AirbaseName )
  end
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


do -- Zones

  --- Finds a @{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 @{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 @{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
  
  --- Finds an @{Zone} based on the zone name in the DATABASE.
  -- @param #DATABASE self
  -- @param #string ZoneName
  -- @return Core.Zone#ZONE_BASE The found @{Zone}.
  function DATABASE:FindZone( ZoneName )
  
    local ZoneFound = self.ZONES[ZoneName]
    return ZoneFound
  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

      self:I( { "Register ZONE:", Name = ZoneName } )
      local Zone = ZONE:New( ZoneName )
      self.ZONENAMES[ZoneName] = ZoneName
      self:AddZone( ZoneName, Zone )
    end
  
    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 "" )
        
        self:I( { "Register ZONE_POLYGON:", Name = ZoneName } )
        local Zone_Polygon = ZONE_POLYGON:New( ZoneName, ZoneGroup )
        self.ZONENAMES[ZoneName] = ZoneName
        self:AddZone( ZoneName, Zone_Polygon )
      end
    end
    
  end


end -- zone


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 Wrapper.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
      self:I( { Cargo = CargoGroupName } )
      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
-- @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
function DATABASE:AddClient( ClientName )

  if not self.CLIENTS[ClientName] then
    self.CLIENTS[ClientName] = CLIENT:Register( ClientName )
  end

  return self.CLIENTS[ClientName]
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 )

  local GroupFound = self.GROUPS[GroupName]
  return GroupFound
end


--- Adds a GROUP based on the GroupName in the DATABASE.
-- @param #DATABASE self
function DATABASE:AddGroup( GroupName )

  if not self.GROUPS[GroupName] then
    self:I( { "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 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

--- Deletes a player from the DATABASE based on the Player Name.
-- @param #DATABASE self
function DATABASE:DeletePlayer( UnitName, PlayerName )

  if PlayerName then
    self:I( { "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  )

  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.
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.
function DATABASE:GetStatusGroup( GroupName )
  self:F2( Status )

  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
-- @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
    
    UnitNames[#UnitNames+1] = self.Templates.Units[UnitTemplate.name].UnitName 
  end

  self:I( { 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

function DATABASE:GetGroupTemplate( GroupName )
  local 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
  return GroupTemplate
end

--- Private method that registers new Static Templates within the DATABASE Object.
-- @param #DATABASE self
-- @param #table StaticTemplate
-- @return #DATABASE self
function DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID )

  local StaticTemplate = UTILS.DeepCopy( StaticTemplate )

  local StaticTemplateName = env.getValueDictByKey(StaticTemplate.name)
  
  self.Templates.Statics[StaticTemplateName] = self.Templates.Statics[StaticTemplateName] or {}
  
  StaticTemplate.CategoryID = CategoryID
  StaticTemplate.CoalitionID = CoalitionID
  StaticTemplate.CountryID = CountryID
  
  self.Templates.Statics[StaticTemplateName].StaticName = StaticTemplateName
  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

  self:I( { 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 )
  
end


--- @param #DATABASE self
function DATABASE:GetStaticGroupTemplate( StaticName )
  local StaticTemplate = self.Templates.Statics[StaticName].GroupTemplate
  return StaticTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
end

--- @param #DATABASE self
function DATABASE:GetStaticUnitTemplate( StaticName )
  local UnitTemplate = self.Templates.Statics[StaticName].UnitTemplate
  return UnitTemplate, self.Templates.Statics[StaticName].CoalitionID, self.Templates.Statics[StaticName].CategoryID, self.Templates.Statics[StaticName].CountryID
end


function DATABASE:GetGroupNameFromUnitName( UnitName )
  return self.Templates.Units[UnitName].GroupName
end

function DATABASE:GetGroupTemplateFromUnitName( UnitName )
  return self.Templates.Units[UnitName].GroupTemplate
end

function DATABASE:GetCoalitionFromClientTemplate( ClientName )
  return self.Templates.ClientsByName[ClientName].CoalitionID
end

function DATABASE:GetCategoryFromClientTemplate( ClientName )
  return self.Templates.ClientsByName[ClientName].CategoryID
end

function DATABASE:GetCountryFromClientTemplate( ClientName )
  return self.Templates.ClientsByName[ClientName].CountryID
end

--- Airbase

function DATABASE:GetCoalitionFromAirbase( AirbaseName )
  return self.AIRBASES[AirbaseName]:GetCoalition()
end

function DATABASE:GetCategoryFromAirbase( AirbaseName )
  return self.AIRBASES[AirbaseName]:GetCategory()
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 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
        local DCSGroupName = DCSGroup:getName()
  
        self:I( { "Register Group:", DCSGroupName } )
        self:AddGroup( DCSGroupName )

        for DCSUnitId, DCSUnit in pairs( DCSGroup:getUnits() ) do
  
          local DCSUnitName = DCSUnit:getName()
          self:I( { "Register Unit:", DCSUnitName } )
          self:AddUnit( DCSUnitName )
        end
      else
        self:E( { "Group does not exist: ",  DCSGroup } )
      end
      
    end
  end
  
  self:I("Groups:")
  for GroupName, Group in pairs( self.GROUPS ) do
    self:I( { "Group:", GroupName } )
  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( { "Register Client:", ClientName } )
    self:AddClient( ClientName )
  end
  
  return self
end

--- @param #DATABASE self
function DATABASE:_RegisterStatics()

  local CoalitionsData = { GroupsRed = coalition.getStaticObjects( coalition.side.RED ), GroupsBlue = coalition.getStaticObjects( coalition.side.BLUE ) }
  self:I( { Statics = CoalitionsData } )
  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
    for DCSStaticId, DCSStatic in pairs( CoalitionData ) do

      if DCSStatic:isExist() then
        local DCSStaticName = DCSStatic:getName()
  
        self:I( { "Register Static:", DCSStaticName } )
        self:AddStatic( DCSStaticName )
      else
        self:E( { "Static does not exist: ",  DCSStatic } )
      end
    end
  end

  return self
end

--- @param #DATABASE self
function DATABASE:_RegisterAirbases()

  local CoalitionsData = { AirbasesRed = coalition.getAirbases( coalition.side.RED ), AirbasesBlue = coalition.getAirbases( coalition.side.BLUE ), AirbasesNeutral = coalition.getAirbases( coalition.side.NEUTRAL ) }
  for CoalitionId, CoalitionData in pairs( CoalitionsData ) do
    for DCSAirbaseId, DCSAirbase in pairs( CoalitionData ) do

      local DCSAirbaseName = DCSAirbase:getName()

      self:I( { "Register Airbase:", DCSAirbaseName, DCSAirbase:getID() } )
      self:AddAirbase( DCSAirbaseName )
    end
  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:F2( { Event } )

  if Event.IniDCSUnit then
    if Event.IniObjectCategory == 3 then
      self:AddStatic( Event.IniDCSUnitName )    
    else
      if Event.IniObjectCategory == 1 then
        self:AddUnit( Event.IniDCSUnitName )
        self:AddGroup( Event.IniDCSGroupName )
      end
    end
    if Event.IniObjectCategory == 1 then
      Event.IniUnit = self:FindUnit( Event.IniDCSUnitName )
      Event.IniGroup = self:FindGroup( Event.IniDCSGroupName )
      local PlayerName = Event.IniUnit:GetPlayerName()
      if PlayerName then
        self:I( { "Player Joined:", PlayerName } )
        if not self.PLAYERS[PlayerName] then
          self:AddPlayer( Event.IniUnitName, PlayerName )
        end
        local Settings = SETTINGS:Set( PlayerName )
        Settings:SetPlayerMenu( Event.IniUnit )
        --MENU_INDEX:Refresh( Event.IniGroup )
      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 )
  self:F2( { Event } )

  if Event.IniDCSUnit then
    if Event.IniObjectCategory == 3 then
      if self.STATICS[Event.IniDCSUnitName] then
        self:DeleteStatic( Event.IniDCSUnitName )
      end    
    else
      if Event.IniObjectCategory == 1 then
        if self.UNITS[Event.IniDCSUnitName] then
          self:DeleteUnit( Event.IniDCSUnitName )
        end
      end
    end
  end
  
  self:AccountDestroys( Event )
end


--- Handles the OnPlayerEnterUnit event to fill the active players table (with the unit filter applied).
-- @param #DATABASE self
-- @param Core.Event#EVENTDATA Event
function DATABASE:_EventOnPlayerEnterUnit( Event )
  self:F2( { Event } )

  if Event.IniDCSUnit then
    if Event.IniObjectCategory == 1 then
      self:AddUnit( Event.IniDCSUnitName )
      Event.IniUnit = self:FindUnit( Event.IniDCSUnitName )
      self:AddGroup( Event.IniDCSGroupName )
      local PlayerName = Event.IniDCSUnit:getPlayerName()
      if not self.PLAYERS[PlayerName] then
        self:AddPlayer( Event.IniDCSUnitName, PlayerName )
      end
      local Settings = SETTINGS:Set( PlayerName )
      Settings:SetPlayerMenu( Event.IniUnit )
    end
  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 } )

  if Event.IniUnit then
    if Event.IniObjectCategory == 1 then
      local PlayerName = Event.IniUnit:GetPlayerName()
      if PlayerName and self.PLAYERS[PlayerName] then
        self:I( { "Player Left:", PlayerName } )
        local Settings = SETTINGS:Set( PlayerName )
        Settings:RemovePlayerMenu( Event.IniUnit )
        self:DeletePlayer( Event.IniUnit, PlayerName )
      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 )
  
  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, ... )
  self:F2( arg )
  
  self:ForEach( IteratorFunction, 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, ... )
  self:F2( arg )
  
  self:ForEach( IteratorFunction, 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 )
  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




--- @param #DATABASE self
function DATABASE:_RegisterTemplates()
  self:F2()

  self.Navpoints = {}
  self.UNITS = {}
  --Build routines.db.units and 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.NEUTRAL
      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] = routines.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.IniCoalition 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 = TargetUnit:getCategory()
      --TargetCategory = TargetUnit:getDesc().category  -- Workaround
      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 colleciton 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 @{Client}s filterd 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.
-- 
-- 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.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Core.Set
-- @image Core_Sets.JPG


do -- SET_BASE

  --- @type SET_BASE
  -- @field #table Filter
  -- @field #table Set
  -- @field #table List
  -- @field Core.Scheduler#SCHEDULER CallScheduler
  -- @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 interator 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.SetInteratorIntervals} 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 = {},
  }
  
  
  --- 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
  
  --- 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
  end
  
  --- Gets a list of the Names of the Objects in the Set.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  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
  
  
  --- Gets a list of the Objects in the Set.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  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 NoTriggerEvent (optional) When `true`, the :Remove() method will not trigger a **Removed** event.
  function SET_BASE:Remove( ObjectName, NoTriggerEvent )
    self:F2( { ObjectName = ObjectName } )
  
    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 not NoTriggerEvent 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
  -- @param Core.Base#BASE Object
  -- @return Core.Base#BASE The added BASE Object.
  function SET_BASE:Add( ObjectName, Object )
    self:F2( { 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
    self.Set[ObjectName] = Object
    table.insert( self.Index, ObjectName )
    
    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 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
  
  
  
  
  --- 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 ObjectName = self.Index[#self.Index]
    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
  function SET_BASE:GetRandom()
  
    local RandomItem = self.Set[self.Index[math.random(#self.Index)]]
    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 #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 = routines.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 **"yield interval"** and the **"time interval"**.
  -- @param #SET_BASE self
  -- @param #number YieldInterval Sets the frequency when the iterator loop will yield after the number of objects processed. The default frequency is 10 objects processed.
  -- @param #number TimeInterval Sets the time in seconds when the main logic will resume the iterator loop. The default time is 0.001 seconds.
  -- @return #SET_BASE self
  function SET_BASE:SetIteratorIntervals( YieldInterval, TimeInterval )
  
    self.YieldInterval = YieldInterval
    self.TimeInterval = TimeInterval
    
    return self
  end
  
  
  --- Filters for the defined collection.
  -- @param #SET_BASE self
  -- @return #SET_BASE self
  function SET_BASE:FilterOnce()
  
    for ObjectName, Object in pairs( self.Database ) do
  
      if self:IsIncludeObject( Object ) then
        self:Add( ObjectName, Object )
      end
    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:E( { "Adding Object:", ObjectName } )
        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 from a @{Core.Point#POINT_VEC2}.
  -- @param #SET_BASE self
  -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set.
  -- @return Core.Base#BASE The closest object.
  function SET_BASE:FindNearestObjectFromPointVec2( PointVec2 )
    self:F2( PointVec2 )
    
    local NearestObject = nil
    local ClosestDistance = nil
    
    for ObjectID, ObjectData in pairs( self.Set ) do
      if NearestObject == nil then
        NearestObject = ObjectData
        ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() )
      else
        local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetVec2() )
        if Distance < ClosestDistance then
          NearestObject = ObjectData
          ClosestDistance = Distance
        end
      end
    end
    
    return NearestObject
  end
  
  
  
  ----- Private method that registers all alive players in the mission.
  ---- @param #SET_BASE self
  ---- @return #SET_BASE self
  --function SET_BASE:_RegisterPlayers()
  --
  --  local CoalitionsData = { AlivePlayersRed = coalition.getPlayers( coalition.side.RED ), AlivePlayersBlue = coalition.getPlayers( coalition.side.BLUE ) }
  --  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()
  --        if not self.PlayersAlive[UnitName] then
  --          self:E( { "Add player for unit:", UnitName, UnitData:getPlayerName() } )
  --          self.PlayersAlive[UnitName] = UnitData:getPlayerName()
  --        end
  --      end
  --    end
  --  end
  --  
  --  return self
  --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.
  -- @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 ), 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 call an interator 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 interator 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 interator 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
  
  --- 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

end


do -- SET_GROUP

  --- @type SET_GROUP
  -- @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 gruops belonging to the country(ies).
  --    * @{#SET_GROUP.FilterPrefixes}: Builds the SET_GROUP with the groups starting with the same prefix string(s).
  --    * @{#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.
  -- 
  --   
  -- 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**.
  -- 
  -- Planned filter criteria within development are (so these are not yet available):
  -- 
  --    * @{#SET_GROUP.FilterZones}: Builds the SET_GROUP with the groups within a @{Core.Zone#ZONE}.
  -- 
  -- ## 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 @{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 @{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 @{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 paramters 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.
  -- Imageine 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,
    },
    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
  end
  
  --- Gets the Set.
  -- @param #SET_GROUP self
  -- @return #SET_GROUP self
  function SET_GROUP:GetAliveSet()
    self:F2()
    
    local AliveSet = SET_GROUP: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 {}
  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.
  -- @return self
  function SET_GROUP:AddGroup( group )
  
    self:Add( group:GetName(), group )
    
    -- I set the default cargo bay weight limit each time a new group is added to the set.
    for UnitID, UnitData in pairs( group:GetUnits() ) do
      UnitData:SetCargoBayWeightLimit()
    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 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 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#POINT_VEC2}.
  -- @param #SET_GROUP self
  -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest object in the set.
  -- @return Wrapper.Group#GROUP The closest group.
  function SET_GROUP:FindNearestGroupFromPointVec2( PointVec2 )
    self:F2( PointVec2 )
    
    local NearestGroup = nil --Wrapper.Group#GROUP
    local ClosestDistance = nil
    
    for ObjectID, ObjectData in pairs( self.Set ) do
      if NearestGroup == nil then
        NearestGroup = ObjectData 
        ClosestDistance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() )
      else
        local Distance = PointVec2:DistanceFromPointVec2( ObjectData:GetCoordinate() )
        if Distance < ClosestDistance then
          NearestGroup = ObjectData
          ClosestDistance = Distance
        end
      end
    end
    
    return NearestGroup
  end
  
  
  --- 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".
  -- @return #SET_GROUP self
  function SET_GROUP: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 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".
  -- @return #SET_GROUP self
  function SET_GROUP: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 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 of defined GROUP prefixes.
  -- All the groups starting with the given prefixes will be included within the set.
  -- @param #SET_GROUP self
  -- @param #string Prefixes The prefix of which the group name starts with.
  -- @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
  
  --- Builds a set of groups that are only active.
  -- 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
    
  
  --- 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 )
    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
        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_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 == 1 then
      if not self.Database[Event.IniDCSGroupName] then
        self.Database[Event.IniDCSGroupName] = GROUP:Register( Event.IniDCSGroupName )
        self:T3( 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 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:ForEachGroup( IteratorFunction, ... )
    self:F2( arg )
    
    self:ForEach( 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 each **alive** GROUP presence completely in a @{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 @{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 @{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 ZoneObject The Zone to be tested for.
  -- @return #boolean true if all the @{Wrapper.Group#GROUP} are completly 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 ZoneObject The Zone to be tested for.
  -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is completly 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 @{GROUP} of the @{SET_GROUP} is in @{ZONE}
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly 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 @{GROUP} of the @{SET_GROUP} is partly in @{ZONE}.
  -- Will return false if a @{GROUP} is fully in the @{ZONE}
  -- @param #SET_GROUP self
  -- @param Core.Zone#ZONE ZoneObject The Zone to be tested for.
  -- @return #boolean true if at least one of the @{Wrapper.Group#GROUP} is partly or completly 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 @{GROUP} of the @{SET_GROUP} is in @{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 ZoneObject 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 ZoneObject 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 ZoneObject 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 call an interator 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 interator 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.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 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 then
      local MGroupCategory = false
      for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
        self:T3( { "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
    end
    
    if self.Filter.Countries 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 then
      local MGroupPrefix = false
      for GroupPrefixId, GroupPrefix in pairs( self.Filter.GroupPrefixes ) do
        self:T3( { "Prefix:", string.find( MGroup:GetName(), GroupPrefix, 1 ), GroupPrefix } )
        if string.find( MGroup:GetName(), GroupPrefix:gsub ("-", "%%-"), 1 ) then
          MGroupPrefix = true
        end
      end
      MGroupInclude = MGroupInclude and MGroupPrefix
    end
  
    self:T2( MGroupInclude )
    return MGroupInclude
  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
  -- @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 starting with the same prefix string(s).
  --    * @{#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!
  --   
  -- 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**.
  -- 
  -- Planned filter criteria within development are (so these are not yet available):
  -- 
  --    * @{#SET_UNIT.FilterZones}: Builds the SET_UNIT with the units within a @{Core.Zone#ZONE}.
  -- 
  -- ## 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 @{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 @{Zone}, providing the UNIT object and optional parameters to the called function.
  --   
  -- Planned iterators methods in development are (so these are not yet available):
  -- 
  --   * @{#SET_UNIT.ForEachUnitInUnit}: Calls a function for each unit contained within the SET_UNIT.
  --   * @{#SET_UNIT.ForEachUnitCompletelyInZone}: Iterate and call an iterator function for each **alive** UNIT presence completely in a @{Zone}, providing the UNIT and optional parameters to the called function.
  --   * @{#SET_UNIT.ForEachUnitNotInZone}: Iterate and call an iterator function for each **alive** UNIT presence not in a @{Zone}, providing the UNIT and optional parameters to the called function.
  -- 
  -- ## 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 paramters 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.
  -- Imageine 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,
    },
    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 )
  
    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 )
    
    -- Set the default cargo bay limit each time a new unit is added to the set.
    Unit:SetCargoBayWeightLimit()
      
    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 Wrapper.Unit#UNIT RemoveUnitNames A single name or an array of UNIT names.
  -- @return 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 of defined unit prefixes.
  -- All the units starting with the given prefixes will be included within the set.
  -- @param #SET_UNIT self
  -- @param #string Prefixes The prefix of which the unit name starts with.
  -- @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 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 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
  
  
  
  --- 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 )
    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_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 == 1 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 ZoneObject 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
    
  
    --- Check if minimal one element of the SET_UNIT is in the Zone.
    -- @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:ForEachUnitInZone( IteratorFunction, ... )
      self:F2( arg )
      
      self:ForEach( IteratorFunction, arg, self:GetSet() )
    
      return self
    end
    
  
  end
  
  
  --- Iterate the SET_UNIT and call an interator 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
  
  --- Iterate the SET_UNIT **sorted *per Threat Level** and call an interator 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 @{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 @{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  @{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 maxium A2G threat level of the SET_UNIT.
  -- @param #SET_UNIT self
  -- @return #number The maximum threatlevel
  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 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 UnitName, UnitData in pairs( self:GetSet() ) do
    
      local Unit = UnitData -- Wrapper.Unit#UNIT
      local Coordinate = Unit: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_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 @{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 @{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 @{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 @{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 interator 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 interator 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 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 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 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 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 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 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 then
        local MUnitSEAD = false
        if MUnit:HasSEAD() == true then
          self:T3( "SEAD Found" )
          MUnitSEAD = true
        end
        MUnitInclude = MUnitInclude and MUnitSEAD
      end
    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 starting with the same prefix string(s).
  --   
  -- 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.
  -- 
  -- Planned filter criteria within development are (so these are not yet available):
  -- 
  --    * @{#SET_STATIC.FilterZones}: Builds the SET_STATIC with the units within a @{Core.Zone#ZONE}.
  -- 
  -- ## 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_GROUP.ForEachGroupCompletelyInZone}: Iterate the SET_GROUP and call an iterator function for each **alive** GROUP presence completely in a @{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 @{Zone}, providing the GROUP and optional parameters to the called function.
  --   
  -- Planned iterators methods in development are (so these are not yet available):
  -- 
  --   * @{#SET_STATIC.ForEachStaticInZone}: Calls a function for each unit contained within the SET_STATIC.
  --   * @{#SET_STATIC.ForEachStaticCompletelyInZone}: Iterate and call an iterator function for each **alive** STATIC presence completely in a @{Zone}, providing the STATIC and optional parameters to the called function.
  --   * @{#SET_STATIC.ForEachStaticNotInZone}: Iterate and call an iterator function for each **alive** STATIC presence not in a @{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 @{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,
    },
    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 #string 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 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
  
  
  --- 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 units of defined unit prefixes.
  -- All the units starting with the given prefixes will be included within the set.
  -- @param #SET_STATIC self
  -- @param #string Prefixes The prefix of which the unit name starts with.
  -- @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.Crash, 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_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.IniDCSStaticName] then
        self.Database[Event.IniDCSStaticName] = STATIC:Register( Event.IniDCSStaticName )
        self:T3( self.Database[Event.IniDCSStaticName] )
      end
    end
    
    return Event.IniDCSStaticName, self.Database[Event.IniDCSStaticName]
  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.IniDCSStaticName, self.Set[Event.IniDCSStaticName], Event } )
  
  
    return Event.IniDCSStaticName, self.Set[Event.IniDCSStaticName]
  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:IsPatriallyInZone( 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 ZoneObject 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 interator 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 @{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 @{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  @{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 maxium 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:T3( { "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:T3( { "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:T3( { "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:T3( { "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:T3( { "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
  
    self:T2( MStaticInclude )
    return MStaticInclude
  end
  
  
  --- Retrieve the type names of the @{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 @{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
  
end


do -- SET_CLIENT


  --- @type SET_CLIENT
  -- @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 starting with the same prefix string(s).
  --    * @{#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!
  --   
  -- 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**.
  -- 
  -- Planned filter criteria within development are (so these are not yet available):
  -- 
  --    * @{#SET_CLIENT.FilterZones}: Builds the SET_CLIENT with the clients within a @{Core.Zone#ZONE}.
  -- 
  -- ## 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,
    },
    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 name or an array of CLIENT names.
  -- @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 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 of defined client prefixes.
  -- All the clients starting with the given prefixes will be included within the set.
  -- @param #SET_CLIENT self
  -- @param #string Prefixes The prefix of which the client name starts with.
  -- @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 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
  
  
  
  --- Starts the filtering.
  -- @param #SET_CLIENT self
  -- @return #SET_CLIENT self
  function SET_CLIENT:FilterStart()
  
    if _DATABASE then
      self:_FilterStart()
      self:HandleEvent( EVENTS.Birth, self._EventOnBirth )
      self:HandleEvent( EVENTS.Dead, self._EventOnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, 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_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 interator 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 @{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 @{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
  
  ---
  -- @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 ) then
          MClientActive = true
        end
        MClientInclude = MClientInclude and MClientActive
      end
    
      if self.Filter.Coalitions then
        local MClientCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName )
          self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] 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 then
        local MClientCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName )
          self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } )
          if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then
            MClientCategory = true
          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: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 then
        local MClientCountry = false
        for CountryID, CountryName in pairs( self.Filter.Countries ) do
          local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName)
          self:T3( { "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: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
    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 starting with the same prefix string(s).
  --   
  -- 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,
    },
    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 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 clients of defined client prefixes.
  -- All the clients starting with the given prefixes will be included within the set.
  -- @param #SET_PLAYER self
  -- @param #string Prefixes The prefix of which the client name starts with.
  -- @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 )
    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 interator 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 @{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 @{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 then
        local MClientCoalition = false
        for CoalitionID, CoalitionName in pairs( self.Filter.Coalitions ) do
          local ClientCoalitionID = _DATABASE:GetCoalitionFromClientTemplate( MClientName )
          self:T3( { "Coalition:", ClientCoalitionID, self.FilterMeta.Coalitions[CoalitionName], CoalitionName } )
          if self.FilterMeta.Coalitions[CoalitionName] 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 then
        local MClientCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          local ClientCategoryID = _DATABASE:GetCategoryFromClientTemplate( MClientName )
          self:T3( { "Category:", ClientCategoryID, self.FilterMeta.Categories[CategoryName], CategoryName } )
          if self.FilterMeta.Categories[CategoryName] and self.FilterMeta.Categories[CategoryName] == ClientCategoryID then
            MClientCategory = true
          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: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 then
        local MClientCountry = false
        for CountryID, CountryName in pairs( self.Filter.Countries ) do
          local ClientCountryID = _DATABASE:GetCountryFromClientTemplate(MClientName)
          self:T3( { "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: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
    end
    
    self:T2( 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,
    },
    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
  
  --- 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 )
  
      -- 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
  
  --- Starts the filtering.
  -- @param #SET_AIRBASE self
  -- @param Core.Event#EVENT EventData
  -- @return #SET_AIRBASE self
  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
  
  --- 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
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_AIRBASE: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_AIRBASE self
  -- @param Core.Event#EVENTDATA Event
  -- @return #string The name of the AIRBASE
  -- @return #table The AIRBASE
  function SET_AIRBASE:FindInDatabase( Event )
    self:F3( { Event } )
  
    return Event.IniDCSUnitName, self.Database[Event.IniDCSUnitName]
  end
  
  --- Iterate the SET_AIRBASE and call an interator 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#POINT_VEC2}.
  -- @param #SET_AIRBASE self
  -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Wrapper.Airbase#AIRBASE}.
  -- @return Wrapper.Airbase#AIRBASE The closest @{Wrapper.Airbase#AIRBASE}.
  function SET_AIRBASE:FindNearestAirbaseFromPointVec2( PointVec2 )
    self:F2( PointVec2 )
    
    local NearestAirbase = self:FindNearestObjectFromPointVec2( PointVec2 )
    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:T3( { "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 then
        local MAirbaseCategory = false
        for CategoryID, CategoryName in pairs( self.Filter.Categories ) do
          local AirbaseCategoryID = _DATABASE:GetCategoryFromAirbase( MAirbaseName )
          self:T3( { "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
    end
     
    self:T2( 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 prefix string(s).
  --    * @{#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 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 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 Wrapper.Cargo#CARGO RemoveCargoNames A single name or an array of CARGO names.
  -- @return 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 Wrapper.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
  
  
  --- (R2.1) Builds a set of cargos of defined cargo prefixes.
  -- All the cargos starting with the given prefixes will be included within the set.
  -- @param #SET_CARGO self
  -- @param #string Prefixes The prefix of which the cargo name starts with.
  -- @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 interator 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#POINT_VEC2}.
  -- @param #SET_CARGO self
  -- @param Core.Point#POINT_VEC2 PointVec2 A @{Core.Point#POINT_VEC2} object from where to evaluate the closest @{Cargo.Cargo#CARGO}.
  -- @return Wrapper.Cargo#CARGO The closest @{Cargo.Cargo#CARGO}.
  function SET_CARGO:FindNearestCargoFromPointVec2( PointVec2 ) --R2.1
    self:F2( PointVec2 )
    
    local NearestCargo = self:FindNearestObjectFromPointVec2( PointVec2 )
    return NearestCargo
  end
  
  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
  
  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:T3( { "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:T3( { "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:T3( { "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
      
    self:T2( 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 of prefix.
  --   
  -- 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 = {
    },
  }
  
  
  --- 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
  -- @return Core.Zone#ZONE_BASE The random Zone.
  -- @return #nil if no zone in the collection.
  function SET_ZONE: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 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 of defined zone prefixes.
  -- All the zones starting with the given prefixes will be included within the set.
  -- @param #SET_ZONE self
  -- @param #string Prefixes The prefix of which the zone name starts with.
  -- @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 interator 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
  
  
  ---
  -- @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:T3( { "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
     
    self:T2( 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 coordiante 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 that validates the coordinate location.
  -- @return #nil No zone has been found.
  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

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.
-- 
-- ===
--
-- # Demo Missions
--
-- ### [POINT_VEC Demo Missions source code]()
--
-- ### [POINT_VEC Demo Missions, only for beta testers]()
--
-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases)
--
-- ===
--
-- # YouTube Channel
--
-- ### [POINT_VEC YouTube Channel]()
--
-- ===
--
-- ### Authors:
--
--   * FlightControl : Design & Programming
--
-- ### Contributions:
--
-- @module Core.Point
-- @image Core_Coordinate.JPG




do -- COORDINATE

  --- @type COORDINATE
  -- @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.
  --
  -- ## 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.GetNorthCorrection}(): 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 coodinates 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.
  --
  -- 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.ToStringLL}(): Generates a Latutude & Longutude text.
  --
  -- @field #COORDINATE
  COORDINATE = {
    ClassName = "COORDINATE",
  }

  --- @field COORDINATE.WaypointAltType 
  COORDINATE.WaypointAltType = {
    BARO = "BARO",
    RADIO = "RADIO",
  }
  
  --- @field COORDINATE.WaypointAction 
  COORDINATE.WaypointAction = {
    TurningPoint = "Turning Point",
    FlyoverPoint = "Fly Over Point",
    FromParkingArea = "From Parking Area",
    FromParkingAreaHot = "From Parking Area Hot",
    FromRunway = "From Runway",
    Landing = "Landing",
  }

  --- @field COORDINATE.WaypointType 
  COORDINATE.WaypointType = {
    TakeOffParking = "TakeOffParking",
    TakeOffParkingHot = "TakeOffParkingHot",
    TakeOff = "TakeOffParkingHot",
    TurningPoint = "Turning Point",
    Land = "Land",
  }


  --- 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 the Right.
  -- @param DCS#Distance z The z coordinate of the Vec3 point, pointing to the Right.
  -- @return #COORDINATE
  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
  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
  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

    self:F2( self )

    return self

  end

  --- Create a new COORDINATE object from  Vec3 coordinates.
  -- @param #COORDINATE self
  -- @param DCS#Vec3 Vec3 The Vec3 point.
  -- @return #COORDINATE
  function COORDINATE:NewFromVec3( Vec3 ) 

    local self = self:New( Vec3.x, Vec3.y, Vec3.z ) -- #COORDINATE

    self:F2( self )

    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

  --- 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=altitude
    else
      _coord.y=self:GetLandHeight()
    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
  
  --- Returns if the 2 coordinates are at the same 2D position.
  -- @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 True if units were found.
  -- @return True if statics were found.
  -- @return True if scenery objects were found.
  -- @return Unit objects found.
  -- @return Static objects found.
  -- @return Scenery objects found.
  function COORDINATE:ScanObjects(radius, scanunits, scanstatics, scanscenery)
    self:F(string.format("Scanning in radius %.1f m.", radius))

    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 = ZoneObject:getCategory()
        
        -- Check for unit or static objects
        --if (ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive()) then
        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()))
    end
    for _,scenery in pairs(Scenery) do
      self:T(string.format("Scan found scenery %s", scenery:getTypeName()))
    end
    
    return gotunits, gotstatics, gotscenery, Units, Statics, Scenery
  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 )

    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.
  -- @return #COORDINATE The new calculated COORDINATE.
  function COORDINATE:Translate( Distance, Angle )
    local SX = self.x
    local SY = self.z
    local Radians = Angle / 180 * math.pi
    local TX = Distance * math.cos( Radians ) + SX
    local TY = Distance * math.sin( Radians ) + SY

    return COORDINATE:NewFromVec2( { x = TX, y = TY } )
  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 } )

    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
  -- @param DCS#Distance InnerRadius
  -- @return #COORDINATE
  function COORDINATE:GetRandomCoordinateInRadius( OuterRadius, InnerRadius )
    self:F2( { OuterRadius, InnerRadius } )

    return COORDINATE:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) )
  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
  function COORDINATE:GetLandHeight()
    local Vec2 = { x = self.x, y = self.z }
    return land.getHeight( Vec2 )
  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 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 )
    return { x = TargetCoordinate.x - self.x, y = TargetCoordinate.y - self.y, z = TargetCoordinate.z - self.z }
  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.
  function COORDINATE:GetAngleDegrees( DirectionVec3 )
    local AngleRadians = self:GetAngleRadians( DirectionVec3 )
    local Angle = UTILS.ToDegree( AngleRadians )
    return Angle
  end


  --- Return the 2D distance in meters between the target COORDINATE and the COORDINATE.
  -- @param #COORDINATE self
  -- @param #COORDINATE TargetCoordinate The target COORDINATE.
  -- @return DCS#Distance Distance The distance in meters.
  function COORDINATE:Get2DDistance( TargetCoordinate )
    local TargetVec3 = TargetCoordinate:GetVec3()
    local SourceVec3 = self:GetVec3()
    return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5
  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 @{Settings}.
  -- The text will reflect the temperature like this:
  -- 
  --   - For Russian and European aircraft using the metric system - Degrees Celcius (°C)
  --   - For Americain aircraft we link to the imperial system - Degrees Farenheit (°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 @{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.CelciusToFarenheit( 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 @{Settings}.
  -- The text will contain always the pressure in hPa and:
  -- 
  --   - For Russian and European aircraft using the metric system - hPa and mmHg
  --   - For Americain 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 @{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 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.
  -- @return Wind strength in m/s.
  function COORDINATE:GetWind(height)
    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.
    local point={x=self.x, y=math.max(height or self.y, landheight), z=self.z}
    -- get wind velocity vector
    local wind = atmosphere.getWind(point)    
    local direction = math.deg(math.atan2(wind.z, wind.x))
    if direction < 0 then
      direction = 360 + direction
    end
    -- Convert to direction to from direction 
    if direction > 180 then
      direction = direction-180
    else
      direction = direction+180
    end
    local strength=math.sqrt((wind.x)^2+(wind.z)^2)
    -- Return wind direction and strength km/h.
    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  


  --- Returns a text documenting the wind direction (from) and strength according the measurement system @{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 Americain 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 @{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.
  -- @return DCS#Distance Distance The distance in meters.
  function COORDINATE:Get3DDistance( TargetCoordinate )
    local TargetVec3 = TargetCoordinate:GetVec3()
    local SourceVec3 = self:GetVec3()
    return ( ( TargetVec3.x - SourceVec3.x ) ^ 2 + ( TargetVec3.y - SourceVec3.y ) ^ 2 + ( TargetVec3.z - SourceVec3.z ) ^ 2 ) ^ 0.5
  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
  -- @return #string The bearing text in degrees.
  function COORDINATE:GetBearingText( AngleRadians, Precision, Settings )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS

    local AngleDegrees = UTILS.Round( UTILS.ToDegree( AngleRadians ), Precision )
  
    local s = string.format( '%03d°', AngleDegrees ) 
    
    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
  -- @return #string The distance text expressed in the units of measurement.
  function COORDINATE:GetDistanceText( Distance, Settings )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS

    local DistanceText

    if Settings:IsMetric() then
      DistanceText = " for " .. UTILS.Round( Distance / 1000, 2 ) .. " km"
    else
      DistanceText = " for " .. UTILS.Round( UTILS.MetersToNM( Distance ), 2 ) .. " miles"
    end
    
    return DistanceText
  end

  --- Return the altitude text of the COORDINATE.
  -- @param #COORDINATE self
  -- @return #string Altitude text.
  function COORDINATE:GetAltitudeText( Settings )
    local Altitude = self.y
    local Settings = Settings or _SETTINGS
    if Altitude ~= 0 then
      if Settings:IsMetric() then
        return " at " .. UTILS.Round( self.y, -3 ) .. " meters"
      else
        return " at " .. UTILS.Round( UTILS.MetersToFeet( self.y ), -3 ) .. " feet"
      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
  -- @return #string The BR Text
  function COORDINATE:GetBRText( AngleRadians, Distance, Settings )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS

    local BearingText = self:GetBearingText( AngleRadians, 0, Settings )
    local DistanceText = self:GetDistanceText( Distance, Settings )
    
    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
  -- @return #string The BRA Text
  function COORDINATE:GetBRAText( AngleRadians, Distance, Settings )

    local Settings = Settings or _SETTINGS -- Core.Settings#SETTINGS

    local BearingText = self:GetBearingText( AngleRadians, 0, Settings )
    local DistanceText = self:GetDistanceText( Distance, Settings )
    local AltitudeText = self:GetAltitudeText( Settings )

    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

  --- Add a Distance in meters from the COORDINATE horizontal 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.
  -- @return #COORDINATE The new calculated COORDINATE.
  function COORDINATE:Translate( Distance, Angle )
    local SX = self.x
    local SZ = self.z
    local Radians = Angle / 180 * math.pi
    local TX = Distance * math.cos( Radians ) + SX
    local TZ = Distance * math.sin( Radians ) + SZ

    return COORDINATE:New( TX, self.y, TZ )
  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.
  -- @return #table The route point.
  function COORDINATE:WaypointAir( AltType, Type, Action, Speed, SpeedLocked, airbase, DCSTasks, description )
    self:F2( { AltType, Type, Action, Speed, SpeedLocked } )
    
    -- Defaults
    AltType=AltType or "RADIO"
    if SpeedLocked==nil then
      SpeedLocked=true
    end
    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
    -- Set speed/ETA.
    RoutePoint.speed = Speed/3.6
    RoutePoint.speed_locked = SpeedLocked
    RoutePoint.ETA=nil
    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:GetDesc().category
      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:T("ERROR: Unknown airbase category in COORDINATE:WaypointAir()!")
      end  
    end        
    

    --  ["task"] =
    --  {
    --      ["id"] = "ComboTask",
    --      ["params"] =
    --      {
    --          ["tasks"] =
    --          {
    --          }, -- end of ["tasks"]
    --      }, -- end of ["params"]
    --  }, -- end of ["task"]

    -- Waypoint tasks.
    RoutePoint.task = {}
    RoutePoint.task.id = "ComboTask"
    RoutePoint.task.params = {}
    RoutePoint.task.params.tasks = DCSTasks or {}

    self:T({RoutePoint=RoutePoint})
    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.
  -- @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 )
    return self:WaypointAir( nil, COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, Speed )
  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".
  -- @return #table The route point.
  function COORDINATE:WaypointGround( Speed, Formation )
    self:F2( { Formation, Speed } )

 
    local RoutePoint = {}
    RoutePoint.x = self.x
    RoutePoint.y = self.z

    RoutePoint.action = Formation or ""
    --RoutePoint.formation_template = Formation and "" or nil


    RoutePoint.speed = ( Speed or 20 ) / 3.6
    RoutePoint.speed_locked = true

    --  ["task"] =
    --  {
    --      ["id"] = "ComboTask",
    --      ["params"] =
    --      {
    --          ["tasks"] =
    --          {
    --          }, -- end of ["tasks"]
    --      }, -- end of ["params"]
    --  }, -- end of ["task"]


    RoutePoint.task = {}
    RoutePoint.task.id = "ComboTask"
    RoutePoint.task.params = {}
    RoutePoint.task.params.tasks = {}


    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
      local category=airbase:GetDesc().category
      if Category and Category==category or Category==nil then
        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
    
    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.
  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

    -- 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
          else    
            if _dist<_distmin then
              _distmin=_dist
              _closest=_coord
              _termID=_spot.TerminalID
            end
          end
                          
        end         
      end
    end
   
    return _closest, _termID, _distmin
  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 vec2={ x = x, y = y }
    return COORDINATE:NewFromVec2(vec2)
  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
        
        if MarkPath then
          coord:MarkToAll(string.format("Path segment %d.", _i))
        end
        if SmokePath then
          coord:SmokeGreen()
        end
      end
            
      -- Mark/smoke endpoints
      if IncludeEndpoints then
        if MarkPath then
          COORDINATE:NewFromVec2(path[1]):MarkToAll("Path Initinal Point")
          COORDINATE:NewFromVec2(path[1]):MarkToAll("Path Final Point")        
        end
        if SmokePath then
          COORDINATE:NewFromVec2(path[1]):SmokeBlue()
          COORDINATE:NewFromVec2(path[#path]):SmokeBlue()
        end
      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
    
    -- 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
    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 road.
  -- @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.
  function COORDINATE:Explosion( ExplosionIntensity )
    self:F2( { ExplosionIntensity } )
    trigger.action.explosion( self:GetVec3(), ExplosionIntensity )
  end

  --- Creates an illumination bomb at the point.
  -- @param #COORDINATE self
  -- @param #number power
  function COORDINATE:IlluminationBomb(power)
    self:F2()
    trigger.action.illuminationBomb( self:GetVec3(), power )
  end


  --- Smokes the point in a color.
  -- @param #COORDINATE self
  -- @param Utilities.Utils#SMOKECOLOR SmokeColor
  function COORDINATE:Smoke( SmokeColor )
    self:F2( { SmokeColor } )
    trigger.action.smoke( self:GetVec3(), SmokeColor )
  end

  --- Smoke the COORDINATE Green.
  -- @param #COORDINATE self
  function COORDINATE:SmokeGreen()
    self:F2()
    self:Smoke( SMOKECOLOR.Green )
  end

  --- Smoke the COORDINATE Red.
  -- @param #COORDINATE self
  function COORDINATE:SmokeRed()
    self:F2()
    self:Smoke( SMOKECOLOR.Red )
  end

  --- Smoke the COORDINATE White.
  -- @param #COORDINATE self
  function COORDINATE:SmokeWhite()
    self:F2()
    self:Smoke( SMOKECOLOR.White )
  end

  --- Smoke the COORDINATE Orange.
  -- @param #COORDINATE self
  function COORDINATE:SmokeOrange()
    self:F2()
    self:Smoke( SMOKECOLOR.Orange )
  end

  --- Smoke the COORDINATE Blue.
  -- @param #COORDINATE self
  function COORDINATE:SmokeBlue()
    self:F2()
    self:Smoke( SMOKECOLOR.Blue )
  end

  --- Big smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @param Utilities.Utils#BIGSMOKEPRESET preset Smoke preset (0=small smoke and fire, 1=medium smoke and fire, 2=large smoke and fire, 3=huge smoke and fire, 4=small smoke, 5=medium smoke, 6=large smoke, 7=huge smoke).
  -- @param #number density (Optional) Smoke density. Number in [0,...,1]. Default 0.5.
  function COORDINATE:BigSmokeAndFire( preset, density )
    self:F2( { preset=preset, density=density } )
    density=density or 0.5
    trigger.action.effectSmokeBig( self:GetVec3(), preset, density )
  end

  --- Small smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeAndFireSmall( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmokeAndFire, density)
  end

  --- Medium smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeAndFireMedium( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmokeAndFire, density)
  end
  
  --- Large smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeAndFireLarge( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmokeAndFire, density)
  end

  --- Huge smoke and fire at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeAndFireHuge( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmokeAndFire, density)
  end
  
  --- Small smoke at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeSmall( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.SmallSmoke, density)
  end
  
  --- Medium smoke at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeMedium( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.MediumSmoke, density)
  end

  --- Large smoke at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeLarge( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.LargeSmoke, density)
  end
  
  --- Huge smoke at the coordinate.
  -- @param #COORDINATE self
  -- @number density (Optional) Smoke density. Number between 0 and 1. Default 0.5.
  function COORDINATE:BigSmokeHuge( density )
    self:F2( { density=density } )
    density=density or 0.5
    self:BigSmokeAndFire(BIGSMOKEPRESET.HugeSmoke, density)
  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 >>>
    --   RemoveMark( MarkID ) -- The mark is now removed
    function COORDINATE:RemoveMark( MarkID )
      trigger.action.removeMark( MarkID )
    end
  
  end -- Markings
  

  --- Returns if a Coordinate has Line of Sight (LOS) with the ToCoordinate.
  -- @param #COORDINATE self
  -- @param #COORDINATE ToCoordinate
  -- @return #boolean true If the ToCoordinate has LOS with the Coordinate, otherwise false.
  function COORDINATE:IsLOS( ToCoordinate )

    -- Measurement of visibility should not be from the ground, so Adding a hypotethical 2 meters to each Coordinate.
    local FromVec3 = self:GetVec3()
    FromVec3.y = FromVec3.y + 2

    local ToVec3 = ToCoordinate:GetVec3()
    ToVec3.y = ToVec3.y + 2

    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 ToCoordinate 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


  --- 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.
  -- @return #string The BR text.
  function COORDINATE:ToStringBR( FromCoordinate, Settings )
    local DirectionVec3 = FromCoordinate:GetDirectionVec3( self )
    local AngleRadians =  self:GetAngleRadians( DirectionVec3 )
    local Distance = self:Get2DDistance( FromCoordinate )
    return "BR, " .. self:GetBRText( AngleRadians, Distance, Settings )
  end

  --- Return a BRAA 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.
  -- @return #string The BR text.
  function COORDINATE:ToStringBRA( FromCoordinate, Settings )
    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 )
  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.
  -- @return #string The BR text.
  function COORDINATE:ToStringBULLS( Coalition, Settings )
    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 )
  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

  --- 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 ) --R2.1 Fixes issue #424.

    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 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 refrence coordinate.
  -- @param #string ReferenceName The refrence 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.
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringFromRP( ReferenceCoord, ReferenceName, Controllable, Settings )
  
    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 ) .. " 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 ) .. " 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.
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringA2G( Controllable, Settings ) 
  
    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 ) 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.
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToStringA2A( Controllable, Settings ) -- R2.2
  
    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 ) 
      else
        return self:ToStringMGRS( Settings )
      end
    end
    if Settings:IsA2A_BULLS() then
      local Coalition = Controllable:GetCoalition()
      return self:ToStringBULLS( Coalition, Settings )
    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
  -- @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 Tasking.Task#TASK Task The task for which coordinates need to be calculated.
  -- @return #string The coordinate Text in the configured coordinate system.
  function COORDINATE:ToString( Controllable, Settings, Task )
  
    self:F2( { Controllable = Controllable and Controllable:GetName() } )

    local Settings = Settings or ( Controllable and _DATABASE:GetPlayerSettings( Controllable:GetPlayerName() ) ) or _SETTINGS

    local ModeA2A = false
    self:E('A2A false')
    
    if Task then
      self:E('Task ' .. Task.ClassName )
      if Task:IsInstanceOf( TASK_A2A ) then
        ModeA2A = true
        self:E('A2A true')
      else
        if Task:IsInstanceOf( TASK_A2G ) then
          ModeA2A = false
        else
          if Task:IsInstanceOf( TASK_CARGO ) then
            ModeA2A = false
          else
            ModeA2A = false
          end
        end
      end
    else
      local IsAir = Controllable and Controllable:IsAirPlane() 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 ) -- R2.3
  
    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

end

do -- POINT_VEC3

  --- 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 coordiante 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.
  --
  -- **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

  --- Create a new POINT_VEC3 object from Vec2 coordinates.
  -- @param #POINT_VEC3 self
  -- @param DCS#Vec2 Vec2 The Vec2 point.
  -- @param DCS#Distance LandHeightAdd (optional) Add a landheight.
  -- @return Core.Point#POINT_VEC3 self
  function POINT_VEC3:NewFromVec2( Vec2, LandHeightAdd )

    local self = BASE:Inherit( self, COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) ) -- Core.Point#POINT_VEC3
    self:F2( self )

    return self
  end


  --- Create a new POINT_VEC3 object from  Vec3 coordinates.
  -- @param #POINT_VEC3 self
  -- @param DCS#Vec3 Vec3 The Vec3 point.
  -- @return Core.Point#POINT_VEC3 self
  function POINT_VEC3:NewFromVec3( Vec3 )

    local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- Core.Point#POINT_VEC3
    self:F2( self )
  
    return self
  end



  --- Return the x coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @return #number The x coodinate.
  function POINT_VEC3:GetX()
    return self.x
  end

  --- Return the y coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @return #number The y coodinate.
  function POINT_VEC3:GetY()
    return self.y
  end

  --- Return the z coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @return #number The z coodinate.
  function POINT_VEC3:GetZ()
    return self.z
  end

  --- Set the x coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param #number x The x coordinate.
  -- @return #POINT_VEC3
  function POINT_VEC3:SetX( x )
    self.x = x
    return self
  end

  --- Set the y coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param #number y The y coordinate.
  -- @return #POINT_VEC3
  function POINT_VEC3:SetY( y )
    self.y = y
    return self
  end

  --- Set the z coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param #number z The z coordinate.
  -- @return #POINT_VEC3
  function POINT_VEC3:SetZ( z )
    self.z = z
    return self
  end

  --- Add to the x coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param #number x The x coordinate value to add to the current x coodinate.
  -- @return #POINT_VEC3
  function POINT_VEC3:AddX( x )
    self.x = self.x + x
    return self
  end

  --- Add to the y coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param #number y The y coordinate value to add to the current y coodinate.
  -- @return #POINT_VEC3
  function POINT_VEC3:AddY( y )
    self.y = self.y + y
    return self
  end

  --- Add to the z coordinate of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param #number z The z coordinate value to add to the current z coodinate.
  -- @return #POINT_VEC3
  function POINT_VEC3:AddZ( z )
    self.z = self.z +z
    return self
  end

  --- Return a random POINT_VEC3 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC3.
  -- @param #POINT_VEC3 self
  -- @param DCS#Distance OuterRadius
  -- @param DCS#Distance InnerRadius
  -- @return #POINT_VEC3
  function POINT_VEC3:GetRandomPointVec3InRadius( OuterRadius, InnerRadius )

    return POINT_VEC3:NewFromVec3( self:GetRandomVec3InRadius( OuterRadius, InnerRadius ) )
  end

end

do -- POINT_VEC2

  --- @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.
  --
  -- ## 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

  --- Create a new POINT_VEC2 object from  Vec2 coordinates.
  -- @param #POINT_VEC2 self
  -- @param DCS#Vec2 Vec2 The Vec2 point.
  -- @return Core.Point#POINT_VEC2 self
  function POINT_VEC2:NewFromVec2( Vec2, LandHeightAdd )

    local LandHeight = land.getHeight( Vec2 )

    LandHeightAdd = LandHeightAdd or 0
    LandHeight = LandHeight + LandHeightAdd

    local self = BASE:Inherit( self, COORDINATE:NewFromVec2( Vec2, LandHeightAdd ) ) -- #POINT_VEC2
    self:F2( self )

    return self
  end

  --- Create a new POINT_VEC2 object from  Vec3 coordinates.
  -- @param #POINT_VEC2 self
  -- @param DCS#Vec3 Vec3 The Vec3 point.
  -- @return Core.Point#POINT_VEC2 self
  function POINT_VEC2:NewFromVec3( Vec3 )

    local self = BASE:Inherit( self, COORDINATE:NewFromVec3( Vec3 ) ) -- #POINT_VEC2
    self:F2( self )

    return self
  end

  --- Return the x coordinate of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @return #number The x coodinate.
  function POINT_VEC2:GetX()
    return self.x
  end

  --- Return the y coordinate of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @return #number The y coodinate.
  function POINT_VEC2:GetY()
    return self.z
  end

  --- Set the x coordinate of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @param #number x The x coordinate.
  -- @return #POINT_VEC2
  function POINT_VEC2:SetX( x )
    self.x = x
    return self
  end

  --- Set the y coordinate of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @param #number y The y coordinate.
  -- @return #POINT_VEC2
  function POINT_VEC2:SetY( y )
    self.z = y
    return self
  end

  --- Return Return the Lat(itude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.x).
  -- @param #POINT_VEC2 self
  -- @return #number The x coodinate.
  function POINT_VEC2:GetLat()
    return self.x
  end

  --- Set the Lat(itude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.x).
  -- @param #POINT_VEC2 self
  -- @param #number x The x coordinate.
  -- @return #POINT_VEC2
  function POINT_VEC2:SetLat( x )
    self.x = x
    return self
  end

  --- Return the Lon(gitude) coordinate of the POINT_VEC2 (ie: (parent)POINT_VEC3.z).
  -- @param #POINT_VEC2 self
  -- @return #number The y coodinate.
  function POINT_VEC2:GetLon()
    return self.z
  end

  --- Set the Lon(gitude) coordinate of the POINT_VEC2 (ie: POINT_VEC3.z).
  -- @param #POINT_VEC2 self
  -- @param #number y The y coordinate.
  -- @return #POINT_VEC2
  function POINT_VEC2:SetLon( z )
    self.z = z
    return self
  end

  --- Return the altitude (height) of the land at the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @return #number The land altitude.
  function POINT_VEC2:GetAlt()
    return self.y ~= 0 or land.getHeight( { x = self.x, y = self.z } )
  end

  --- Set the altitude of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @param #number Altitude The land altitude. If nothing (nil) is given, then the current land altitude is set.
  -- @return #POINT_VEC2
  function POINT_VEC2:SetAlt( Altitude )
    self.y = Altitude or land.getHeight( { x = self.x, y = self.z } )
    return self
  end

  --- Add to the x coordinate of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @param #number x The x coordinate.
  -- @return #POINT_VEC2
  function POINT_VEC2:AddX( x )
    self.x = self.x + x
    return self
  end

  --- Add to the y coordinate of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @param #number y The y coordinate.
  -- @return #POINT_VEC2
  function POINT_VEC2:AddY( y )
    self.z = self.z + y
    return self
  end

  --- Add to the current land height an altitude.
  -- @param #POINT_VEC2 self
  -- @param #number Altitude The Altitude to add. If nothing (nil) is given, then the current land altitude is set.
  -- @return #POINT_VEC2
  function POINT_VEC2:AddAlt( Altitude )
    self.y = land.getHeight( { x = self.x, y = self.z } ) + Altitude or 0
    return self
  end


  --- Return a random POINT_VEC2 within an Outer Radius and optionally NOT within an Inner Radius of the POINT_VEC2.
  -- @param #POINT_VEC2 self
  -- @param DCS#Distance OuterRadius
  -- @param DCS#Distance InnerRadius
  -- @return #POINT_VEC2
  function POINT_VEC2:GetRandomPointVec2InRadius( OuterRadius, InnerRadius )
    self:F2( { OuterRadius, InnerRadius } )

    return POINT_VEC2:NewFromVec2( self:GetRandomVec2InRadius( OuterRadius, InnerRadius ) )
  end

  -- TODO: Check this to replace
  --- Calculate the distance from a reference @{#POINT_VEC2}.
  -- @param #POINT_VEC2 self
  -- @param #POINT_VEC2 PointVec2Reference The reference @{#POINT_VEC2}.
  -- @return DCS#Distance The distance from the reference @{#POINT_VEC2} in meters.
  function POINT_VEC2:DistanceFromPointVec2( PointVec2Reference )
    self:F2( PointVec2Reference )

    local Distance = ( ( PointVec2Reference.x - self.x ) ^ 2 + ( PointVec2Reference.z - self.z ) ^2 ) ^ 0.5

    self:T2( Distance )
    return Distance
  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 @{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 @{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 @{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 an @{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 @{Settings}.
  -- @param #VELOCITY_POSITIONABLE self
  -- @return #string The velocity in text according the player or default @{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.
-- 
-- ===
-- 
-- @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 @{Client} using @{#MESSAGE.ToClient}().
--   * To a @{Wrapper.Group} using @{#MESSAGE.ToGroup}()
--   * 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: 
-- 
-- ===
-- 
-- @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 @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients.
-- @param self
-- @param #string MessageText is the text of the Message.
-- @param #number MessageDuration is a number in seconds of how long the MESSAGE should be shown on the display panel.
-- @param #string MessageCategory (optional) is a 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
-- @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( MessageText, MessageDuration, MessageCategory, ClearScreen )
	local self = BASE:Inherit( self, BASE:New() )
	self:F( { MessageText, MessageDuration, MessageCategory } )


  self.MessageType = nil
  
  -- When no MessageCategory is given, we don't show it as a title...	
	if MessageCategory and MessageCategory ~= "" then
	  if MessageCategory:sub(-1) ~= "\n" then
      self.MessageCategory = MessageCategory .. ": "
    else
      self.MessageCategory = MessageCategory:sub( 1, -2 ) .. ":\n" 
    end
  else
    self.MessageCategory = ""
  end
  
  self.ClearScreen=false
  if ClearScreen~=nil then
    self.ClearScreen=ClearScreen
  end

	self.MessageDuration = MessageDuration or 5
	self.MessageTime = timer.getTime()
	self.MessageText = MessageText: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 @{ToClient} or @{ToCoalition} or @{ToAll} to send these Messages to the respective recipients.
-- The message display times are automatically defined based on the timing settings in the @{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 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.
-- ClientGroup = Group.getByName( "ClientGroup" )
--
-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup )
-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup )
-- or
-- MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" ):ToClient( ClientGroup )
-- MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" ):ToClient( ClientGroup )
-- or
-- MessageClient1 = MESSAGE:New( "Congratulations, you've just hit a target", "Score", 25, "Score" )
-- MessageClient2 = MESSAGE:New( "Congratulations, you've just killed a target", "Score", 25, "Score" )
-- MessageClient1:ToClient( ClientGroup )
-- MessageClient2:ToClient( ClientGroup )
function MESSAGE:ToClient( Client, Settings )
	self:F( Client )

	if Client and Client:GetClientGroupID() then

    if self.MessageType then
      local Settings = Settings or ( Client and _DATABASE:GetPlayerSettings( Client:GetPlayerName() ) ) or _SETTINGS -- Core.Settings#SETTINGS
      self.MessageDuration = Settings:GetMessageTime( self.MessageType )
      self.MessageCategory = "" -- self.MessageType .. ": "
    end

    if self.MessageDuration ~= 0 then
  		local ClientGroupID = Client:GetClientGroupID()
  		self:T( self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$","") .. " / " .. self.MessageDuration )
  		trigger.action.outTextForGroup( ClientGroupID, self.MessageCategory .. self.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration , self.ClearScreen)
		end
	end
	
	return self
end

--- Sends a MESSAGE to a Group. 
-- @param #MESSAGE self
-- @param Wrapper.Group#GROUP Group to which the message is displayed.
-- @return #MESSAGE Message object.
function MESSAGE:ToGroup( Group, Settings )
  self:F( Group.GroupName )

  if Group 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 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", "Penalty", 25, "Score" ):ToBlue()
-- or
-- MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToBlue()
-- or
-- MessageBLUE = MESSAGE:New( "To the BLUE Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" )
-- 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", "Penalty", 25, "Score" ):ToRed()
-- or
-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):ToRed()
-- or
-- MessageRED = MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" )
-- 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", "Penalty", 25, "Score" ):ToCoalition( coalition.side.RED )
-- or
-- MESSAGE:New( "To the RED Players: You receive a penalty because you've killed one of your own units", "Penalty", 25, "Score" ):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", "Penalty", 25, "Score" )
-- 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.MessageText:gsub("\n$",""):gsub("\n$",""), self.MessageDuration, self.ClearScreen )
    end
	end
	
	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 @{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.
-- @return #MESSAGE
-- @usage
-- -- Send a message created to all players.
-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll()
-- or
-- MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" ):ToAll()
-- or
-- MessageAll = MESSAGE:New( "To all Players: BLUE has won! Each player of BLUE wins 50 points!", "End of Mission", 25, "Win" )
-- MessageAll:ToAll()
function MESSAGE:ToAll(Settings)
  self:F()

  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

  return self
end


--- Sends a MESSAGE to all players if the given Condition is true.
-- @param #MESSAGE self
-- @return #MESSAGE
function MESSAGE:ToAllIf( Condition )

  if Condition and Condition == true then
    self:ToAll()
  end

	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**. 
-- 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** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class.
--   * an acronym **TASK\_**, which indicates an FSM implementation executing a @{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 @{TASK}, seated in a @{CLIENT} (slot) or a @{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.
-- 
-- ##__Dislaimer:__
-- 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 specialised form of a FSM:
-- 
--   * @{#FSM_TASK}: Models Finite State Machines for @{Task}s.
--   * @{#FSM_PROCESS}: Models Finite State Machines for @{Task} actions, which control @{Client}s.
--   * @{#FSM_CONTROLLABLE}: Models Finite State Machines for @{Wrapper.Controllable}s, which are @{Wrapper.Group}s, @{Wrapper.Unit}s, @{Client}s.
--   * @{#FSM_SET}: Models Finite State Machines for @{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: 
-- 
-- ===
--
-- @module Core.Fsm
-- @image Core_Finite_State_Machine.JPG

do -- FSM

  --- @type FSM
  -- @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** @{GROUP} and/or @{UNIT}. These AI\_ classes derive the @{#FSM_CONTROLLABLE} class.
  --   * an acronym **TASK\_**, which indicates an FSM implementation executing a @{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 @{TASK}, seated in a @{CLIENT} (slot) or a @{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 seperate 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 Lineair 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 consensed.
  -- 
  --      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 destinction 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/blob/master/Moose%20Test%20Missions/FSM%20-%20Finite%20State%20Machine/FSM-100%20-%20Transition%20Explanation/FSM-100%20-%20Transition%20Explanation.lua)
  -- 
  -- 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
  
    self:T2( Transition )
    
    self._Transitions[Transition] = Transition
    self:_eventmap( self.Events, Transition )
  end

  
  --- Returns a table of the transition rules defined within the FSM.
  -- @return #table
  function FSM:GetTransitions()
  
    return self._Transitions or {}
  end
  
  --- Set the default @{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:T( { 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.
  -- @return #table
  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.
  function FSM:AddEndState( State )
  
    self._EndStates[State] = State
    self.endstates[State] = State
  end
  
  --- Returns the 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:T( Process._Scores )
  
    return Process
  end
  
  --- Returns a table with the scores defined.
  function FSM:GetScores()
  
    return self._Scores or {}
  end
  
  --- Returns a table with the Subs defined.
  function FSM:GetSubs()
  
    return self.options.subs
  end
  
  
  function FSM:LoadCallBacks( CallBackTable )
  
    for name, callback in pairs( CallBackTable or {} ) do
      self[name] = callback
    end
  
  end
  
  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)
      self:T2( "Added methods: " .. Event .. ", " .. __Event )
      Events[Event] = self.Events[Event] or { map = {} }
      self:_add_to_map( Events[Event].map, EventStructure )
  
  end
  
  function FSM:_submap( subs, sub, name )
    --self:F( { sub = sub, name = 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
  
  
  function FSM:_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] )
      self._EventSchedules[EventName] = nil
      local Result, Value = xpcall( function() return self[handler]( self, unpack( params ) ) end, ErrorHandler )
      return Value
    end
  end
  
  --- @param #FSM self
  function FSM._handler( self, EventName, ... )
  
    local Can, To = self:can( EventName )
  
    if To == "*" then
      To = self.current
    end
  
    if Can then
      local From = self.current
      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
  
      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 )
      
        -- only execute the call if the From state is not equal to the To state! Otherwise this function should never execute!
        --if from ~= to then
          self:_call_handler("onenter", To, Params, EventName )
          self:_call_handler("OnEnter", To, Params, EventName )
        --end
    
        self:_call_handler("onstate", "change", Params, EventName )
      end
    else
      self:T( "*** FSM *** NO Transition *** " .. self.current .. " --> " .. EventName .. " -->  ? " )
    end
  
    return nil
  end
  
  function FSM:_delayed_transition( EventName )
    return function( self, DelaySeconds, ... )
      self:T2( "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
            CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 )
            self._EventSchedules[EventName] = CallID
          else
            -- reschedule
          end
        else
          CallID = self.CallScheduler:Schedule( self, self._handler, { EventName, ... }, DelaySeconds or 1 )
        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
      self:T2( { CallID = CallID } )
    end
  end
  
  function FSM:_create_transition( EventName )
    return function( self, ... ) return self._handler( self,  EventName , ... ) end
  end
  
  function FSM:_gosub( ParentFrom, ParentEvent )
    local fsmtable = {}
    if self.subs[ParentFrom] and self.subs[ParentFrom][ParentEvent] then
      self:T( { ParentFrom, ParentEvent, self.subs[ParentFrom], self.subs[ParentFrom][ParentEvent] } )
      return self.subs[ParentFrom][ParentEvent]
    else
      return {}
    end
  end
  
  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
  
  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
  
  function FSM:GetState()
    return self.current
  end
  
  function FSM:GetCurrentState()
    return self.current
  end
  
  
  function FSM:Is( State )
    return self.current == State
  end
  
  function FSM:is(state)
    return self.current == state
  end
  
  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
  
  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, @{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 @{Task} actions, which control @{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:T( { 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:T( EndState )
      NewFsm:AddEndState( EndState )
    end
    
    -- Copy the score tables
    for ScoreID, Score in pairs( self:GetScores() ) do
      self:T( 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 @{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 Task.Tasking#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 @{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.Controllable
  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** - Is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions.
-- 
-- ===
-- 
-- ## Features:
-- 
--   * Provide radio functionality to broadcast radio transmissions.
--   * Provide beacon functionality to assist pilots.
--
-- The Radio contains 2 classes : RADIO and BEACON
--  
-- 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 transmiter'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 (wich can be decompressed like a .zip file),
--   * For simplicty 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 a FC3 airacraft 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 aircaft isn't compatible,
-- you won't hear/be able to use the TACAN beacon informations.
--
-- ===
--
-- ### Author: Hugues "Grey_Echo" Bousquet
--
-- @module Core.Radio
-- @image Core_Radio.JPG


--- Models the radio capabilty.
-- 
-- ## 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 transmiter 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 transmiter 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 transmiter, 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 100W antenna, a standard AA TACAN has a 120W antenna, and civilian ATC's antenna usually range between 300 and 500W,
--   * Note that if the transmission has a subtitle, it will be readable, regardless of the quality of the transmission. 
--   
-- @type RADIO
-- @field Positionable#POSITIONABLE Positionable The transmiter
-- @field #string FileName Name of the sound file
-- @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 (default true)
-- @extends Core.Base#BASE
RADIO = {
  ClassName = "RADIO",
  FileName = "",
  Frequency = 0,
  Modulation = radio.modulation.AM,
  Subtitle = "",
  SubtitleDuration = 0,
  Power = 100,
  Loop = true,
}

--- 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 @{Positionable} that will receive radio capabilities.
-- @return #RADIO Radio
-- @return #nil If Positionable is invalid
function RADIO:New(Positionable)
  local self = BASE:Inherit( self, BASE:New() ) -- Core.Radio#RADIO
  
  self.Loop = true        -- default Loop to true (not sure the above RADIO definition actually is working)
  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({"The passed positionable is invalid, no RADIO created", Positionable})
  return nil
end

--- Check validity of the filename passed and sets RADIO.FileName
-- @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 ?", self.FileName})
  return self
end

--- Check validity of the frequency passed and sets RADIO.Frequency
-- @param #RADIO self
-- @param #number Frequency in MHz (Ranges allowed for radio transmissions in DCS : 30-88 / 108-152 / 225-400MHz)
-- @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 < 88) or (Frequency >= 108 and Frequency < 152) or (Frequency >= 225 and Frequency < 400) then
      self.Frequency = Frequency * 1000000 -- Conversion in Hz
      -- 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
        self.Positionable:SetCommand({
          id = "SetFrequency",
          params = {
            frequency = self.Frequency,
            modulation = self.Modulation,
          }
        })
      end
      return self
    end
  end
  self:E({"Frequency is outside of DCS Frequency ranges (30-80, 108-152, 225-400). Frequency unchanged.", self.Frequency})
  return self
end

--- Check validity of the frequency passed and sets RADIO.Modulation
-- @param #RADIO self
-- @param #number Modulation 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
      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 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
    return self
  end
  self:E({"Power is invalid. Power unchanged.", self.Power})
  return self
end

--- Check validity of the loop passed and sets RADIO.Loop
-- @param #RADIO self
-- @param #boolean Loop
-- @return #RADIO self
-- @usage
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
    if math.floor(math.abs(SubtitleDuration)) == SubtitleDuration then
      self.SubtitleDuration = SubtitleDuration
      return self
    end
  end
  self.SubtitleDuration = 0
  self:E({"SubtitleDuration is invalid. SubtitleDuration reset.", self.SubtitleDuration})
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
-- @param #number Frequency in MHz
-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM
-- @param #number 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
-- @param #string Subtitle
-- @param #number SubtitleDuration in s
-- @param #number Frequency in MHz
-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM
-- @param #boolean Loop
-- @return #RADIO self
function RADIO:NewUnitTransmission(FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop)
  self:F({FileName, Subtitle, SubtitleDuration, Frequency, Modulation, Loop})

  self:SetFileName(FileName)
  local Duration = 5
  if SubtitleDuration then Duration = SubtitleDuration end
  -- SubtitleDuration argument was missing, adding it
  if Subtitle then self:SetSubtitle(Subtitle, Duration) end
  -- self:SetSubtitleDuration is non existent, removing faulty line
  -- if SubtitleDuration then self:SetSubtitleDuration(SubtitleDuration) end
  if Frequency then self:SetFrequency(Frequency) end
  if Modulation then self:SetModulation(Modulation) end
  if Loop then self:SetLoop(Loop) end
  
  return self
end

--- Actually 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
-- @return #RADIO self
function RADIO:Broadcast()
  self:F()
  
  -- 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" then
    self:T2("Broadcasting from a UNIT or a GROUP")
    self.Positionable:SetCommand({
      id = "TransmitMessage",
      params = {
        file = self.FileName,
        duration = self.SubtitleDuration,
        subtitle = self.Subtitle,
        loop = self.Loop,
      }
    })
  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:T2("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 usefull 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
    self.Positionable:SetCommand({
      id = "StopTransmission",
      params = {}
    })
  else
    -- Else, we use the appropriate singleton funciton
    trigger.action.stopRadioTransmission(tostring(self.ID))
  end
  return self
end


--- 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 AA 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 usefull to simulate the battery time if your BEACON is
-- attach to a cargo crate, for exemple. 
-- 
-- ## AA TACAN Beacon usage
-- 
-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon.
-- Use @#BEACON:StopAATACAN}() 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
-- @extends Core.Base#BASE
BEACON = {
  ClassName = "BEACON",
}

--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.AATACAN} or @{#BEACON.Generic}
-- 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 @{Positionable} that will receive radio capabilities.
-- @return #BEACON Beacon
-- @return #nil If Positionable is invalid
function BEACON:New(Positionable)
  local self = BASE:Inherit(self, BASE:New())
  
  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({"The passed positionable is invalid, no BEACON created", Positionable})
  return nil
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


--- 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})
  
  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 = 5
  else
    System = 14
  end
  
  if IsValid then -- Starts the BEACON
    self:T2({"AA TACAN BEACON started !"})
    self.Positionable:SetCommand({
      id = "ActivateBeacon",
      params = {
        type = 4,
        system = System,
        callsign = Message,
        frequency = Frequency,
        }
      })
      
    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 pupose 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 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. 
-- They 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
  
  -- 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
    trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID))
    
     if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD
       SCHEDULER:New( nil, 
         function()
           self:StopRadioBeacon()
         end, {}, BeaconDuration)
     end
  end 
end

--- Stops the AA TACAN 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(tostring(self.ID))
end--- **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 intial positions within the zones.
--   * Spawn in array formation.
--   * Spawn uncontrolled (for planes or helos 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.
--   * 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-release/SPA%20-%20Spawning)
-- 
-- ===
-- 
-- ### [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 @{Core.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 behaviour 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 batallion in an array.
-- 
-- ### 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 @{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 inital delay On/Off when scheduled spawning the first @{Wrapper.Group} object.
--   * @{#SPAWN.InitDelayOn}(): Turns the inital delay On when scheduled spawning the first @{Wrapper.Group} object.
--   * @{#SPAWN.InitDelayOff}(): Turns the inital 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 **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 @{Static}.
--   * @{#SPAWN.SpawnFromUnit}(): Spawn a new group taking the position of a @{Wrapper.Unit}.
--   * @{#SPAWN.SpawnInZone}(): Spawn a new group in a @{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 @{GROUP} object to do further actions with the DCSGroup.
-- 
-- ### **Scheduled** spawning methods
-- 
--   * @{#SPAWN.SpawnScheduled}(): Spawn groups at scheduled but randomized intervals. 
--   * @{#SPAWN.SpawnScheduledStart}(): Start or continue to spawn groups at scheduled time intervals. 
--   * @{#SPAWN.SpawnScheduledStop}(): 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 damged 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 succesfully 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 behaviour 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.SpawnScheduledStop}() ; @{#SPAWN.SpawnScheduledStart}() sequence would have been used.
-- 
-- 
-- @field #SPAWN SPAWN
-- 
SPAWN = {
  ClassName = "SPAWN",
  SpawnTemplatePrefix = nil,
  SpawnAliasPrefix = nil,
}


--- Enumerator for spawns at airbases
-- @type SPAWN.Takeoff
-- @extends Wrapper.Group#GROUP.Takeoff

--- @field #SPAWN.Takeoff Takeoff
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 intial delay when spawning the first group.
    self.Grouping = nil                             -- No grouping.
    self.SpawnInitLivery = nil                      -- No special livery.
    self.SpawnInitSkill = nil                       -- No special skill.

		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
-- @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 intial delay when spawning the first group.
    self.Grouping = nil                             -- No grouping.
    self.SpawnInitLivery = nil                      -- No special livery.
    self.SpawnInitSkill = nil                       -- No special skill.

		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.
-- @param #SPAWN self
-- @param #table SpawnTemplate is the Template of the Group. This must be a valid Group Template structure!
-- @param #string SpawnTemplatePrefix is the name of the Group that will be given at each spawn.
-- @param #string SpawnAliasPrefix (optional) is the name that will be given to the Group at runtime.
-- @return #SPAWN
-- @usage
-- -- Create a new SPAWN object based on a Group Template defined from scratch.
-- Spawn_BE_KA50 = SPAWN:NewWithAlias( 'BE KA-50@RAMP-Ground Defense', 'Helicopter Attacking a City' )
-- @usage 
-- -- Create a new CSAR_Spawn object based on a normal Group Template to spawn a soldier.
-- local CSAR_Spawn = SPAWN:NewWithFromTemplate( Template, "CSAR", "Pilot" )
function SPAWN:NewFromTemplate( SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix )
  local self = BASE:Inherit( self, BASE:New() )
  self:F( { SpawnTemplate, SpawnTemplatePrefix, SpawnAliasPrefix } )
  
  if SpawnTemplate then
    self.SpawnTemplate = 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
    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 intial delay when spawning the first group.
    self.Grouping = nil                             -- No grouping.
    self.SpawnInitLivery = nil                      -- No special livery.
    self.SpawnInitSkill = nil                       -- No special skill.

    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


--- Limits the Maximum amount of Units that can be alive at the same time, and the maximum amount of groups that can be spawned.
-- 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 to make new unit names.
-- @return #SPAWN self
function SPAWN:InitKeepUnitNames( KeepUnitNames )
  self:F( )

  self.SpawnInitKeepUnitNames = KeepUnitNames or true
  
  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


--- 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


--- 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#coaliton.side.NEUTRAL}
--   * @{DCS#coaliton.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 determins the coalition of the group depending on which country is defined to be on which side for each specific mission!
-- @param #SPAWN self 
-- @param #DCS.country Country Country id as number or enumerator:
-- 
--   * @{DCS#country.id.RUSSIA}
--   * @{DCS#county.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 edior.
-- @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


--- Randomizes the defined route of the SpawnTemplatePrefix group in the ME. This is very useful to define extra variation of the behaviour 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 @{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. 
-- -- 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: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

--- 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 #string SpawnTemplatePrefixTable A table with the names of the groups defined within the mission editor, from which one will be choosen 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 simulteneously  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 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 )
-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 )
-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplate( Spawn_US_Platoon ):InitRandomizeRoute( 3, 3, 2000 )
function SPAWN:InitRandomizeTemplate( SpawnTemplatePrefixTable )
	self:F( { self.SpawnTemplatePrefix, SpawnTemplatePrefixTable } )

	self.SpawnTemplatePrefixTable = SpawnTemplatePrefixTable
	self.SpawnRandomizeTemplate = true

	for SpawnGroupID = 1, self.SpawnMaxGroups do
		self:_RandomizeTemplate( SpawnGroupID )
	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. 
-- @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 simulteneously  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 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 )
-- Spawn_US_Platoon_Middle = SPAWN:New( 'US Tank Platoon Middle' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 )
-- Spawn_US_Platoon_Right = SPAWN:New( 'US Tank Platoon Right' ):InitLimit( 12, 150 ):Schedule( 200, 0.4 ):InitRandomizeTemplateSet( Spawn_US_PlatoonSet ):InitRandomizeRoute( 3, 3, 2000 )
function SPAWN:InitRandomizeTemplateSet( SpawnTemplateSet ) -- R2.3
  self:F( { self.SpawnTemplatePrefix } )

  self.SpawnTemplatePrefixTable = SpawnTemplateSet:GetSetNames()
  self.SpawnRandomizeTemplate = true

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeTemplate( SpawnGroupID )
  end
  
  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. 
-- @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 simulteneously  and 150 Groups to be spawned during the whole mission.
-- 
-- Spawn_US_Platoon_Left = SPAWN:New( 'US Tank Platoon Left' ):InitLimit( 12, 150 ):Schedule( 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 ):Schedule( 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 ):Schedule( 200, 0.4 ):InitRandomizeTemplatePrefixes( "US Tank Platoon Templates" ):InitRandomizeRoute( 3, 3, 2000 )
function SPAWN:InitRandomizeTemplatePrefixes( SpawnTemplatePrefixes ) --R2.3
  self:F( { self.SpawnTemplatePrefix } )

  local SpawnTemplateSet = SET_GROUP:New():FilterPrefixes( SpawnTemplatePrefixes ):FilterOnce()

  self:InitRandomizeTemplateSet( SpawnTemplateSet )
  
  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 @{Zone} objects. If this table is given, then each spawn will be executed within the given list of @{Zone}s objects. 
-- @return #SPAWN
-- @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 )
  self:F( { self.SpawnTemplatePrefix, SpawnZoneTable } )

  self.SpawnZoneTable = SpawnZoneTable
  self.SpawnRandomizeZones = true

  for SpawnGroupID = 1, self.SpawnMaxGroups do
    self:_RandomizeZones( 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
-- @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 )
--   :InitRepeatOnLanding()
--   
function SPAWN:InitRepeatOnLanding()
	self:F( { self.SpawnTemplatePrefix } )

	self:InitRepeat()
	self.RepeatOnEngineShutDown = false
	self.RepeatOnLanding = true
	
	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' )
--   :Schedule( 2, 3, 1800, 0.4 )
--   :SpawnUncontrolled()
--   :InitRandomizeRoute( 1, 1, 3000 )
--   :InitRepeatOnEngineShutDown()
--   
function SPAWN:InitRepeatOnEngineShutDown()
	self:F( { self.SpawnTemplatePrefix } )

	self:InitRepeat()
	self.RepeatOnEngineShutDown = true
	self.RepeatOnLanding = false
	
	return self
end


--- CleanUp groups when they are still alive, but inactive.
-- 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:T( { "CleanUp Scheduler:", SpawnGroup } )
	
	--self.CleanUpFunction = routines.scheduleFunction( self._SpawnCleanUpScheduler, { self }, timer.getTime() + 1, SpawnCleanUpInterval )
	self.CleanUpScheduler = SCHEDULER:New( self, self._SpawnCleanUpScheduler, {}, 1, SpawnCleanUpInterval, 0.2 )
	return self
end



--- Makes the groups visible before start (like a batallion).
-- The method will take the position of the group as the first position in the array.
-- @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:T( { 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 )
    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

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.
  -- @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

--- Will spawn a group based on the internal index.
-- Note: Uses @{DATABASE} module defined 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 } )

	return self:SpawnWithIndex( self.SpawnIndex + 1 )
end

--- Will re-spawn a group based on a given index.
-- Note: Uses @{DATABASE} module defined 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).
	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.ReSpawnFunction then
	  SpawnGroup:ReSpawnFunction()
	end
	
	SpawnGroup:ResetEvents()
	
	return SpawnGroup
end

--- Will spawn a group with a specified index number.
-- Uses @{DATABASE} global object defined 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 )
	self:F2( { SpawnTemplatePrefix = self.SpawnTemplatePrefix, SpawnIndex = SpawnIndex, AliveUnits = self.AliveUnits, SpawnMaxGroups = self.SpawnMaxGroups } )
	
	if self:_GetSpawnIndex( SpawnIndex ) then
		
		if self.SpawnGroups[self.SpawnIndex].Visible then
			self.SpawnGroups[self.SpawnIndex].Group:Activate()
		else

		  local SpawnTemplate = self.SpawnGroups[self.SpawnIndex].SpawnTemplate
		  self:T( SpawnTemplate.name )

      if SpawnTemplate then

        local PointVec3 = POINT_VEC3:New( SpawnTemplate.route.points[1].x, SpawnTemplate.route.points[1].alt, SpawnTemplate.route.points[1].y )
        self:T( { "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:T( '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 )
            SpawnTemplate.units[UnitID].x = RandomVec2.x
            SpawnTemplate.units[UnitID].y = RandomVec2.y
            self:T( 'SpawnTemplate.units['..UnitID..'].x = ' .. SpawnTemplate.units[UnitID].x .. ', SpawnTemplate.units['..UnitID..'].y = ' .. SpawnTemplate.units[UnitID].y )
          end
        end
        
        -- 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        
        
        -- If Heading is given, point all the units towards the given Heading.
        if self.SpawnInitHeadingMin then
          for UnitID = 1, #SpawnTemplate.units do
            SpawnTemplate.units[UnitID].heading = _Heading(self.SpawnInitHeadingMax and math.random( self.SpawnInitHeadingMin, self.SpawnInitHeadingMax ) or self.SpawnInitHeadingMin)
            SpawnTemplate.units[UnitID].psi = -SpawnTemplate.units[UnitID].heading
          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 country, coaliton and categroy.
        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
		  
      self:HandleEvent( EVENTS.Birth, self._OnBirth )
      self:HandleEvent( EVENTS.Dead, self._OnDeadOrCrash )
      self:HandleEvent( EVENTS.Crash, 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 .1 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.1 )
			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
		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.
-- @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 %-tage of variation to be applied on the time interval.
-- @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 choosen for each new spawn of the helicopters.
-- Spawn_BE_KA50 = SPAWN:New( 'BE KA-50@RAMP-Ground Defense' ):Schedule( 600, 0.5 )
function SPAWN:SpawnScheduled( SpawnTime, SpawnTimeVariation )
	self:F( { SpawnTime, SpawnTimeVariation } )

	if SpawnTime ~= nil and SpawnTimeVariation ~= nil then
	  local InitialDelay = 0
	  if 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 exactily these spots!
-- @return Wrapper.Group#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 ) -- R2.2, R2.4
  self:F( { self.SpawnTemplatePrefix, SpawnAirbase, Takeoff, TakeoffAltitude, TerminalType } )

  -- Get position of airbase.
  local PointVec3 = SpawnAirbase:GetCoordinate()
  self:T2(PointVec3)

  -- 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

      -- Debug output
      self:T( { "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:GetDesc().category
      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:T({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 then
        
        -- Number of free parking spots.
        local nfree=0
        
        -- Set terminal type.
        local termtype=TerminalType
        if spawnonrunway then
          termtype=AIRBASE.TerminalType.Runway
        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 spawnonship or spawnonfarp or spawnonrunway then
          -- These places work procedural and have some kind of build in queue ==> Less effort.
          self:T(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:T(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)
              nfree=#spots
              if nfree<nunits then
                -- Not enough helo ports. Let's try also other terminal types.
                self:T(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)
                nfree=#spots
              end
            else
              -- No terminal type specified. We try all spots except shelters.
              self:T(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)
              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:T(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)
                nfree=#spots
                if nfree<nunits then
                  -- Now we try the smaller ones.
                  self:T(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)
                  nfree=#spots
                end
              else
                self:T(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)
                nfree=#spots              
              end            
            else
              -- Terminal type explicitly given.
              self:T(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)
              nfree=#spots
            end
          end
        end
      
        -- 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:T(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 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.
            spawnonground=false
            spawnonship=false
            spawnonfarp=false
            spawnonrunway=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

      -- Translate the position of the Group Template to the Vec3.
      for UnitID = 1, nunits do
        self:T2('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:T(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:T(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:T(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
      
      -- Set gereral spawnpoint position.
      SpawnPoint.x   = PointVec3.x
      SpawnPoint.y   = PointVec3.z
      SpawnPoint.alt = PointVec3.y
      
      SpawnTemplate.x = PointVec3.x
      SpawnTemplate.y = PointVec3.z
      
      -- 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 )
        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

--- 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.
-- @return #nil Nothing was spawned.
function SPAWN:SpawnFromVec3( Vec3, SpawnIndex )
  self:F( { self.SpawnTemplatePrefix, Vec3, SpawnIndex } )

  local PointVec3 = POINT_VEC3: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:T( { "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:T( '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:T( '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.
-- @return #nil 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#POINT_VEC3 PointVec3 The PointVec3 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.
-- @return #nil 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.
-- @return #nil 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 POINT_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 Core.Point#POINT_VEC2 PointVec2 The PointVec2 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.
-- @return #nil 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.
-- @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: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 @{Zone}.
-- The @{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.
-- @return #nil when 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 or true
	
	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:T( SpawnName )
		return SpawnName
	else
		self:T( 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.SpawnTemplatePrefixself.SpawnAliasPrefix } )

  self.SpawnIndex = self:_GetLastIndex()
  for SpawnIndex = self.SpawnIndex, 1, -1 do
    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 DCS#UNIT DCSUnit The @{DCSUnit} to be searched.
-- @return #string The prefix
-- @return #nil Nothing found
function SPAWN:_GetPrefixFromGroup( SpawnGroup )
  self:F3( { self.SpawnTemplatePrefix, self.SpawnAliasPrefix, SpawnGroup } )

  local GroupName = SpawnGroup:GetName()
  if GroupName then
    local SpawnPrefix = string.match( GroupName, ".*#" )
    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:F2( { 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:F( { 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.
-- This method used the @{DATABASE} object, which contains ALL initial and new spawned object 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

  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 = self:_GetTemplate( SpawnTemplatePrefix )
	--local SpawnTemplate = self.SpawnTemplate
	SpawnTemplate.name = self:SpawnGroupName( SpawnIndex )
	
	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
  		SpawnTemplate.units[UnitID].name = string.format( SpawnTemplate.name .. '-%02d', UnitID )
  		SpawnTemplate.units[UnitID].unitId = nil
  	end
  else
    for UnitID = 1, #SpawnTemplate.units do
      local UnitPrefix, Rest = string.match( SpawnTemplate.units[UnitID].name, "^([^#]+)#?" ):gsub( "^%s*(.-)%s*$", "%1" )
      self:T( { UnitPrefix, Rest } )
      
      SpawnTemplate.units[UnitID].name = string.format( '%s#%03d-%02d', UnitPrefix, SpawnIndex, UnitID )
      SpawnTemplate.units[UnitID].unitId = nil
    end
  end
	
	self:T3( { "Template:", SpawnTemplate } )
	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:T( '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 randomizes the @{Zone}s where the Group will be spawned.
-- @param #SPAWN self
-- @param #number SpawnIndex
-- @return #SPAWN self
function SPAWN:_RandomizeZones( SpawnIndex )
  self:F( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnRandomizeZones } )

  if self.SpawnRandomizeZones then
    local SpawnZone = nil -- Core.Zone#ZONE_BASE
    while not SpawnZone do
      self:T( { SpawnZoneTableCount = #self.SpawnZoneTable, self.SpawnZoneTable } )
      local ZoneID = math.random( #self.SpawnZoneTable )
      self:T( ZoneID )
      SpawnZone = self.SpawnZoneTable[ ZoneID ]:GetZoneMaybe() 
    end
    
    self:T( "Preparing Spawn in Zone", SpawnZone:GetName() )
    
    local SpawnVec2 = SpawnZone:GetRandomVec2()
    
    self:T( { SpawnVec2 = SpawnVec2 } )
    
    local SpawnTemplate = self.SpawnGroups[SpawnIndex].SpawnTemplate
    
    self:T( { Route = SpawnTemplate.route } )
    
    for UnitID = 1, #SpawnTemplate.units do
      local UnitTemplate = SpawnTemplate.units[UnitID]
      self:T( '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:T( '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.
function SPAWN:_GetSpawnIndex( SpawnIndex )
	self:F2( { self.SpawnTemplatePrefix, SpawnIndex, self.SpawnMaxGroups, self.SpawnMaxUnitsAlive, self.AliveUnits, #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:F( { 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:T( { "Birth Event:", EventPrefix, self.SpawnTemplatePrefix } )
  		if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then
  			self.AliveUnits = self.AliveUnits + 1
  			self:T( "Alive Units: " .. self.AliveUnits )
  		end
    end
	end

end

--- Obscolete
-- @todo Need to delete this... _DATABASE does this now ...

--- @param #SPAWN self 
-- @param Core.Event#EVENTDATA EventData
function SPAWN:_OnDeadOrCrash( 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:T( { "Dead event: " .. EventPrefix } )
  		if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then
  			self.AliveUnits = self.AliveUnits - 1
  			self:T( "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:T( { "TakeOff event: " .. EventPrefix } )
      if EventPrefix == self.SpawnTemplatePrefix or ( self.SpawnAliasPrefix and EventPrefix == self.SpawnAliasPrefix ) then
    		self:T( "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:T( { "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:T( { "Landed:", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } )
  				self:ReSpawn( SpawnGroupIndex )
  			end
  		end
    end
	end
end

--- Will detect AIR Units shutting down their engines ...
-- When the event takes place, and the method @{RepeatOnEngineShutDown} 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:T( { "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:T( { "EngineShutDown: ", "ReSpawn:", SpawnGroup:GetName(), SpawnGroupIndex } )
  				self:ReSpawn( SpawnGroupIndex )
  			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.
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:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } )

	while SpawnGroup do

    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:T( { SpawnUnitName, Stamp } )
	    
	    if Stamp.Vec2 then
    		if SpawnUnit:InAir() == false and SpawnUnit:GetVelocityKMH() < 1 then
    		  local NewVec2 = SpawnUnit:GetVec2()
    		  if Stamp.Vec2.x == NewVec2.x and Stamp.Vec2.y == NewVec2.y then
      		  -- If the plane is not moving, and is on the ground, assign it with a timestamp...
    				if Stamp.Time + self.SpawnCleanUpInterval < timer.getTime() then
    					self:T( { "CleanUp Scheduler:", "ReSpawning:", SpawnGroup:GetName() } )
    					self:ReSpawn( SpawnCursor )
              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 then
          Stamp.Vec2 = SpawnUnit:GetVec2()
          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:T( { "CleanUp Scheduler:", SpawnGroup, SpawnCursor } )
		
	end
	
	return true -- Repeat
	
end
--- **Core** - Spawn new statics in your running missions.
--  
-- ===
-- 
-- ## Features:
-- 
--   * Spawn new statics from a static already defined using 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.
-- 
-- ===
-- 
-- # Demo Missions
-- 
-- ### [SPAWNSTATIC Demo Missions source code](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/SPS - Spawning Statics)
-- 
-- ### [SPAWNSTATIC Demo Missions, only for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SPS%20-%20Spawning%20Statics)
--
-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases)
-- 
-- ===
-- 
-- # YouTube Channel
-- 
-- ### [SPAWNSTATIC YouTube Channel]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Core.SpawnStatic
-- @image Core_Spawnstatic.JPG



--- @type SPAWNSTATIC
-- @extends Core.Base#BASE


--- Allows to spawn dynamically new @{Static}s.
-- 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 @{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 @{Static}s will follow a naming convention at run-time:
-- 
--   * Spawned @{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 construction methods
-- 
-- Create a new SPAWNSTATIC object with the @{#SPAWNSTATIC.NewFromStatic}():
-- 
--   * @{#SPAWNSTATIC.NewFromStatic}(): Creates a new SPAWNSTATIC object given a name that is used as the base of the naming of each spawned Static.
--
-- ## **Spawn** methods
-- 
-- Groups can be spawned at different times and methods:
-- 
--   * @{#SPAWNSTATIC.SpawnFromPointVec2}(): Spawn a new group from a POINT_VEC2 coordinate. 
--   (The group will be spawned at land height ).
--   * @{#SPAWNSTATIC.SpawnFromZone}(): Spawn a new group in a @{Zone}.
--  
-- @field #SPAWNSTATIC SPAWNSTATIC
-- 
SPAWNSTATIC = {
  ClassName = "SPAWNSTATIC",
}


--- @type SPAWNSTATIC.SpawnZoneTable
-- @list <Core.Zone#ZONE_BASE> SpawnZone


--- Creates the main object to spawn a @{Static} defined in the ME.
-- @param #SPAWNSTATIC 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.
-- @param DCS#country.id SpawnCountryID The ID of the country.
-- @param DCS#coalition.side SpawnCoalitionID The ID of the coalition.
-- @return #SPAWNSTATIC
function SPAWNSTATIC:NewFromStatic( SpawnTemplatePrefix, SpawnCountryID, SpawnCoalitionID )
	local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC
	self:F( { SpawnTemplatePrefix } )
  
	local TemplateStatic, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( SpawnTemplatePrefix )
	if TemplateStatic then
		self.SpawnTemplatePrefix = SpawnTemplatePrefix
		self.CountryID = SpawnCountryID or CountryID
		self.CategoryID = CategoryID
		self.CoalitionID = SpawnCoalitionID or CoalitionID
		self.SpawnIndex = 0
	else
		error( "SPAWNSTATIC:New: There is no static declared in the mission editor with SpawnTemplatePrefix = '" .. SpawnTemplatePrefix .. "'" )
	end

  self:SetEventPriority( 5 )

	return self
end

--- Creates the main object to spawn a @{Static} based on a type name.
-- @param #SPAWNSTATIC self
-- @param #string SpawnTypeName is the name of the type.
-- @return #SPAWNSTATIC
function SPAWNSTATIC:NewFromType( SpawnTypeName, SpawnShapeName, SpawnCategory, SpawnCountryID, SpawnCoalitionID ) 
  local self = BASE:Inherit( self, BASE:New() ) -- #SPAWNSTATIC
  self:F( { SpawnTypeName } )
  
  self.SpawnTypeName = SpawnTypeName
  self.CountryID = SpawnCountryID
  self.CoalitionID = SpawnCoalitionID
  self.SpawnIndex = 0

  self:SetEventPriority( 5 )

  return self
end


--- Creates a new @{Static} at the original position.
-- @param #SPAWNSTATIC self
-- @param #number Heading The heading of the static, which is a number in degrees from 0 to 360.
-- @param #string (optional) The name of the new static.
-- @return #SPAWNSTATIC
function SPAWNSTATIC:Spawn( Heading, NewName ) --R2.3
  self:F( { Heading, NewName  } )
  
  local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix )
  
  if StaticTemplate then
  
    local StaticUnitTemplate = StaticTemplate.units[1]
  
    StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex )
    StaticTemplate.heading = ( Heading / 180 ) * math.pi
    
    _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID )

    local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] )
    
    self.SpawnIndex = self.SpawnIndex + 1
  
    return _DATABASE:FindStatic(Static:getName())
  end
  
  return nil
end


--- Creates a new @{Static} from a POINT_VEC2.
-- @param #SPAWNSTATIC self
-- @param Core.Point#POINT_VEC2 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 (optional) The name of the new static.
-- @return #SPAWNSTATIC
function SPAWNSTATIC:SpawnFromPointVec2( PointVec2, Heading, NewName ) --R2.1
  self:F( { PointVec2, Heading, NewName  } )
  
  local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix )
  
  if StaticTemplate then
  
    local StaticUnitTemplate = StaticTemplate.units[1]
  
    StaticUnitTemplate.x = PointVec2.x
    StaticUnitTemplate.y = PointVec2.z
  
    StaticTemplate.route = nil
    StaticTemplate.groupId = nil
    
    StaticTemplate.name = NewName or string.format("%s#%05d", self.SpawnTemplatePrefix, self.SpawnIndex )
    StaticUnitTemplate.name = StaticTemplate.name
    StaticUnitTemplate.heading = ( Heading / 180 ) * math.pi
    
    _DATABASE:_RegisterStaticTemplate( StaticTemplate, CoalitionID, CategoryID, CountryID)
    
    self:F({StaticTemplate = StaticTemplate})

    local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] )
    
    self.SpawnIndex = self.SpawnIndex + 1
    
    return _DATABASE:FindStatic(Static:getName())
  end
  
  return nil
end


--- Respawns the original @{Static}.
-- @param #SPAWNSTATIC self
-- @return #SPAWNSTATIC
function SPAWNSTATIC:ReSpawn()
  
  local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix )
  
  if StaticTemplate then

    local StaticUnitTemplate = StaticTemplate.units[1]
    StaticTemplate.route = nil
    StaticTemplate.groupId = nil
    
    local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] )
    
    return _DATABASE:FindStatic(Static:getName())
  end
  
  return nil
end


--- Creates the original @{Static} at a POINT_VEC2.
-- @param #SPAWNSTATIC self
-- @param Core.Point#COORDINATE Coordinate 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.
-- @return #SPAWNSTATIC
function SPAWNSTATIC:ReSpawnAt( Coordinate, Heading )
  
  local StaticTemplate, CoalitionID, CategoryID, CountryID = _DATABASE:GetStaticGroupTemplate( self.SpawnTemplatePrefix )
  
  if StaticTemplate then

    local StaticUnitTemplate = StaticTemplate.units[1]
  
    StaticUnitTemplate.x = Coordinate.x
    StaticUnitTemplate.y = Coordinate.z

    StaticUnitTemplate.heading = Heading and ( ( Heading / 180 ) * math.pi ) or StaticTemplate.heading
    
    local Static = coalition.addStaticObject( self.CountryID or CountryID, StaticTemplate.units[1] )
    
    return _DATABASE:FindStatic(Static:getName())
  end
  
  return nil
end


--- Creates a new @{Static} from a @{Zone}.
-- @param #SPAWNSTATIC self
-- @param Core.Zone#ZONE_BASE Zone The Zone 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 (optional) The name of the new static.
-- @return #SPAWNSTATIC
function SPAWNSTATIC:SpawnFromZone( Zone, Heading, NewName ) --R2.1
  self:F( { Zone, Heading, NewName  } )

  local Static = self:SpawnFromPointVec2( Zone:GetPointVec2(), Heading, NewName )
  
  return Static
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**
-- 
-- ===
-- 
-- @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.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 laer 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
-- 
-- ### [SPOT Demo Missions source code]()
-- 
-- ### [SPOT Demo Missions, only for beta testers]()
--
-- ### [ALL Demo Missions pack of the last release](https://github.com/FlightControl-Master/MOOSE_MISSIONS/releases)
-- 
-- ===
-- 
-- # YouTube Channel
-- 
-- ### [SPOT YouTube Channel]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * [**Ciribob**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script.
--   * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing
--   * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): 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 laer 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.
  -- 
  -- ## 1. SPOT constructor
  --   
  --   * @{#SPOT.New}(..\Presentations\SPOT\Dia2.JPG): 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( "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.LaseScheduler = SCHEDULER:New( self )
  
    self:SetEventPriority( 5 )
    
    self.Lasing = false
  
    return self
  end
  
  --- @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:F( { "LaseOn", Target, LaserCode, Duration } )

    local function StopLase( self )
      self:LaseOff()
    end
    
    self.Target = Target
    self.LaserCode = LaserCode
    
    self.Lasing = true
    
    local RecceDcsUnit = self.Recce:GetDCSObject()
    
    self.SpotIR = Spot.createInfraRed( RecceDcsUnit, { x = 0, y = 2, z = 0 }, Target:GetPointVec3():AddY(1):GetVec3() )
    self.SpotLaser = Spot.createLaser( RecceDcsUnit, { x = 0, y = 2, z = 0 }, 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 )
  end

  --- @param #SPOT self
  -- @param Core.Event#EVENTDATA EventData
  function SPOT:OnEventDead(EventData)
    self:F( { Dead = EventData.IniDCSUnitName, Target = self.Target } )
    if self.Target then
      if EventData.IniDCSUnitName == self.Target:GetName() then
        self:F( {"Target dead ", self.Target:GetName() } )
        self:Destroyed()
        self:LaseOff()
      end
    end
  end
  
  --- @param #SPOT self
  -- @param From
  -- @param Event
  -- @param To
  function SPOT:onafterLasing( From, Event, To )
  
    if self.Target:IsAlive() then
      self.SpotIR:setPoint( self.Target:GetPointVec3():AddY(1):AddY(math.random(-100,100)/100):AddX(math.random(-100,100)/100):GetVec3() )
      self.SpotLaser:setPoint( self.Target:GetPointVec3():AddY(1):GetVec3() )
      self:__Lasing( -0.2 )
    else
      self:F( { "Target is not alive", self.Target:IsAlive() } )
    end
  
  end

  --- @param #SPOT self
  -- @param From
  -- @param Event
  -- @param To
  -- @return #SPOT
  function SPOT:onafterLaseOff( From, Event, To )
  
    self:F( {"Stopped lasing for ", self.Target:GetName() , 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
  
end--- **Wrapper** -- OBJECT wraps the DCS Object derived objects.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Wrapper.Object
-- @image MOOSE.JPG


--- @type OBJECT
-- @extends Core.Base#BASE
-- @field #string ObjectName The name of the Object.


--- Wrapper class to hendle 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, Test )
  local self = BASE:Inherit( self, BASE:New() )
  self:F2( ObjectName )
  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 

  BASE:E( { "Cannot GetID", Name = self.ObjectName, Class = self:GetClassName() } )

  return nil
end

--- Destroys the OBJECT.
-- @param #OBJECT self
-- @return #boolean true if the object is destroyed.
-- @return #nil The DCS Unit is not existing or alive.  
function OBJECT:Destroy()

  local DCSObject = self:GetDCSObject()
  
  if DCSObject then
    --BASE:CreateEventCrash( timer.getTime(), DCSObject )
    DCSObject:destroy( false )
    return true
  end

  BASE: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]    = "Helicoper",
  [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.
-- @return #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.
-- @return #nil The DCS Identifiable is not existing or alive.  
function IDENTIFIABLE:GetName()
  self:F2( self.IdentifiableName )

  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.
-- @return #nil The DCS Identifiable is not existing or alive.  
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 category of the DCS Identifiable.
-- @param #IDENTIFIABLE self
-- @return DCS#Object.Category The category ID
function IDENTIFIABLE:GetCategory()
  self:F2( self.ObjectName )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    local ObjectCategory = DCSObject:getCategory()
    self:T3( ObjectCategory )
    return ObjectCategory
  end

  return 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.
-- @return #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
    local IdentifiableCoalition = DCSIdentifiable:getCoalition()
    self:T3( IdentifiableCoalition )
    
    if IdentifiableCoalition == coalition.side.BLUE then
      return "Blue"
    end
    
    if IdentifiableCoalition == coalition.side.RED then
      return "Red"
    end
    
    if IdentifiableCoalition == coalition.side.NEUTRAL then
      return "Neutral"
    end
  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.
-- @return #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.
-- @return #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.
-- @return #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


function IDENTIFIABLE:GetThreatLevel()

  return 0, "Scenery"
end
--- **Wrapper** -- POSITIONABLE wraps DCS classes that are "positionable".
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Wrapper.Positionable
-- @image Wrapper_Positionable.JPG

--- @type POSITIONABLE.__ Methods which are not intended for mission designers, but which are used interally by the moose designer :-)
-- @extends Wrapper.Identifiable#IDENTIFIABLE

--- @type POSITIONABLE
-- @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 = "",
}

--- @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 ) )

  self.PositionableName = PositionableName
  return self
end

--- Destroys the POSITIONABLE.
-- @param #POSITIONABLE self
-- @param #boolean GenerateEvent (Optional) true if you want to generate a crash or dead event for the unit.
-- @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:CreateEventCrash( timer.getTime(), DCSObject )
      else
        self:CreateEventDead( timer.getTime(), DCSObject )
      end
    elseif GenerateEvent == false then
      -- Do nothing!
    else
      self:CreateEventRemoveUnit( timer.getTime(), DCSObject )
    end
    
    USERFLAG:New( UnitGroupName ):Set( 100 )
    DCSObject:destroy()
  end

  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 Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Position Table consisting of the point and orientation tables.
function POSITIONABLE:GetPosition()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable then
    local PositionablePosition = DCSPositionable:getPosition()
    self:T3( PositionablePosition )
    return PositionablePosition
  end
  
  BASE:E( { "Cannot GetPositionVec3", 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 Wrapper.Positionable#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.
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 Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Vec3 X orientation, i.e. parallel to the direction of movement.
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 Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Vec3 Y orientation, i.e. vertical.
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 Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Vec3 Z orientation, i.e. perpendicular to movement.
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 Wrapper.Positionable#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#Vec2} vector indicating the point in 2D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Vec2 The 2D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.  
function POSITIONABLE:GetVec2()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable then
    local PositionableVec3 = DCSPositionable:getPosition().p
    
    local PositionableVec2 = {}
    PositionableVec2.x = PositionableVec3.x
    PositionableVec2.y = PositionableVec3.z
  
    self:T2( PositionableVec2 )
    return PositionableVec2
  end
  
  BASE:E( { "Cannot GetVec2", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a POINT_VEC2 object indicating the point in 2D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return Core.Point#POINT_VEC2 The 2D 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 = POINT_VEC2:NewFromVec3( PositionableVec3 )
  
    --self:F( PositionablePointVec2 )
    return PositionablePointVec2
  end
  
  BASE:E( { "Cannot GetPointVec2", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns a POINT_VEC3 object indicating the point in 3D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return Core.Point#POINT_VEC3 The 3D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.  
function POSITIONABLE:GetPointVec3()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable then
    local PositionableVec3 = self:GetPositionVec3()
    
    local PositionablePointVec3 = POINT_VEC3:NewFromVec3( PositionableVec3 )
  
    self:T2( PositionablePointVec3 )
    return PositionablePointVec3
  end

  BASE:E( { "Cannot GetPointVec3", Positionable = self, Alive = self:IsAlive() } )
  
  return nil
end

--- Returns a COORDINATE object indicating the point in 3D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return Core.Point#COORDINATE The COORDINATE of the POSITIONABLE.
function POSITIONABLE:GetCoordinate()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable then
    local PositionableVec3 = self:GetPositionVec3()
    
    local PositionableCoordinate = COORDINATE:NewFromVec3( PositionableVec3 )
    PositionableCoordinate:SetHeading( self:GetHeading() )
    PositionableCoordinate:SetVelocity( self:GetVelocityMPS() )
  
    self:T2( PositionableCoordinate )
    return PositionableCoordinate
  end
  
  BASE:E( { "Cannot GetCoordinate", Positionable = self, Alive = self:IsAlive() } )
  
  return nil
end


--- Returns a random @{DCS#Vec3} vector within a range, indicating the point in 3D of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#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

--- Returns the @{DCS#Vec3} vector indicating the 3D vector of the POSITIONABLE within the mission.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Vec3 The 3D point vector of the POSITIONABLE.
-- @return #nil The POSITIONABLE is not existing or alive.  
function POSITIONABLE:GetVec3()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable then
    local PositionableVec3 = DCSPositionable:getPosition().p
    self:T3( PositionableVec3 )
    return PositionableVec3
  end
  
  BASE:E( { "Cannot GetVec3", 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() --R2.1
  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 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 or #nil if 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:E( { "Cannot GetBoundingRadius", Positionable = self, Alive = self:IsAlive() } )

  return nil
end

--- Returns the altitude of the POSITIONABLE.
-- @param Wrapper.Positionable#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 Wrapper.Positionable#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 Wrapper.Positionable#POSITIONABLE self
-- @return #number The POSITIONABLE heading
-- @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
      self:T2( PositionableHeading )
      return PositionableHeading
    end
  end
  
  BASE: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.
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
  
  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.
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
  
  return nil
end


--- Returns true if the POSITIONABLE is in the air.
-- Polymorphic, is overridden in GROUP and UNIT.
-- @param Wrapper.Positionable#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 a @{Velocity} object from the positionable.
-- @param Wrapper.Positionable#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 Wrapper.Positionable#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


--- Returns the POSITIONABLE height in meters.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return DCS#Vec3 The height of the positionable.
-- @return #nil The POSITIONABLE is not existing or alive.  
function POSITIONABLE:GetHeight() --R2.1
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable 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 Wrapper.Positionable#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 Wrapper.Positionable#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 Angle of Attack of a positionable.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return #number Angle of attack in degrees.
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 unit's climb or descent angle.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return #number Climb or descent angle in degrees.
function POSITIONABLE:GetClimbAnge()

  -- 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

      return math.asin(unitvel.y/UTILS.VecNorm(unitvel))

    end
  end
end

--- Returns the pitch angle of a unit.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return #number Pitch ange in degrees.
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 Wrapper.Positionable#POSITIONABLE self
-- @return #number Pitch ange in degrees.
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
end

--- Returns the yaw angle of a unit.
-- @param Wrapper.Positionable#POSITIONABLE self
-- @return #number Yaw ange in degrees.
function POSITIONABLE:GetYaw()

  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
  
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, the Name is the type of the Positionable.
-- @return #string The message text
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, the Name is the type of the Positionable.
-- @return Core.Message#MESSAGE
function POSITIONABLE:GetMessage( Message, Duration, Name ) --R2.1 changed callsign and name and using GetMessageText

  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, the Name is 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, the Name is 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, the Name is 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, the Name is 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, the Name is 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, the Name is 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, the Name is 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.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, the Name is 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 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, the Name is the type of the Positionable.
function POSITIONABLE:MessageToSetGroup( Message, Duration, MessageSetGroup, Name )  --R2.1
  self:F2( { Message, Duration } )

  local DCSObject = self:GetDCSObject()
  if DCSObject then
    if DCSObject:isExist() then
      MessageSetGroup:ForEachGroup(
        function( MessageGroup )
          self:GetMessage( Message, Duration, Name ):ToGroup( 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, the Name is 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 @{Core.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 Core.Radio#RADIO Radio
function POSITIONABLE:GetRadio() --R2.1
  self:F2(self)
  return RADIO:New(self) 
end

--- Create a @{Core.Radio#BEACON}, to allow this POSITIONABLE to broadcast beacon signals
-- @param #POSITIONABLE self
-- @return Core.Radio#RADIO Radio
function POSITIONABLE:GetBeacon() --R2.1
  self:F2(self)
  return BEACON:New(self) 
end

--- Start Lasing a POSITIONABLE
-- @param #POSITIONABLE self
-- @param #POSITIONABLE Target
-- @param #number LaserCode
-- @param #number Duration
-- @return Core.Spot#SPOT
function POSITIONABLE:LaseUnit( Target, LaserCode, Duration ) --R2.1
  self:F2()

  LaserCode = LaserCode or math.random( 1000, 9999 )

  local RecceDcsUnit = self:GetDCSObject()
  local TargetVec3 = Target:GetVec3()

  self:F("bulding spot")
  self.Spot = SPOT:New( self ) -- Core.Spot#SPOT
  self.Spot:LaseOn( Target, LaserCode, Duration)
  self.LaserCode = LaserCode
  
  return self.Spot
  
end

--- Stop Lasing a POSITIONABLE
-- @param #POSITIONABLE self
-- @return #POSITIONABLE
function POSITIONABLE:LaseOff() --R2.1
  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() --R2.1
  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() --R2.1
  
  return self.Spot
end

--- Get the last assigned laser code
-- @param #POSITIONABLE self
-- @return #number The laser code
function POSITIONABLE:GetLaserCode() --R2.1
  
  return self.LaserCode
end

do -- Cargo

  --- Add cargo.
  -- @param #POSITIONABLE self
  -- @param Core.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 Core.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 Core.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 Core.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 Cargo Bay Free Volume in m3.
--  -- @param #POSITIONABLE self
--  -- @return #number CargoBayFreeVolume
--  function POSITIONABLE:GetCargoBayFreeVolume()
--    local CargoVolume = 0
--    for CargoName, Cargo in pairs( self.__.Cargo ) do
--      CargoVolume = CargoVolume + Cargo:GetVolume()
--    end
--    return self.__.CargoBayVolumeLimit - CargoVolume
--  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

--  --- Get Cargo Bay Volume Limit in m3.
--  -- @param #POSITIONABLE self
--  -- @param #number VolumeLimit
--  function POSITIONABLE:SetCargoBayVolumeLimit( VolumeLimit )
--    self.__.CargoBayVolumeLimit = VolumeLimit
--  end

  --- Set Cargo Bay Weight Limit in kg.
  -- @param #POSITIONABLE self
  -- @param #number WeightLimit
  function POSITIONABLE:SetCargoBayWeightLimit( WeightLimit )
    
    if WeightLimit then
      self.__.CargoBayWeightLimit = WeightLimit
    elseif self.__.CargoBayWeightLimit~=nil then
      -- Value already set ==> Do nothing!
    else
      -- If weightlimit is not provided, we will calculate it depending on the type of unit.
      
      -- When an airplane or helicopter, we calculate the weightlimit based on the descriptor.
      if self:IsAir() then
        local Desc = self:GetDesc()
        self:F({Desc=Desc})

        local Weights = { 
          ["C-17A"] = 35000,   --77519 cannot be used, because it loads way too much apcs and infantry.,
          ["C-130"] = 22000    --The real value cannot be used, because it loads way too much apcs and infantry.,
        } 

        self.__.CargoBayWeightLimit = Weights[Desc.typeName] or ( Desc.massMax - ( Desc.massEmpty + Desc.fuelMassMax ) )
      else
        local Desc = self:GetDesc()

        local Weights = { 
          ["M1126 Stryker ICV"] = 9,
          ["M-113"] = 9,
          ["AAV7"] = 25,
          ["M2A1_halftrack"] = 9,
          ["BMD-1"] = 9,
          ["BMP-1"] = 8,
          ["BMP-2"] = 7,
          ["BMP-3"] = 8,
          ["Boman"] = 25,
          ["BTR-80"] = 9,
          ["BTR_D"] = 12,
          ["Cobra"] = 8,
          ["LAV-25"] = 6,
          ["M-2 Bradley"] = 6,
          ["M1043 HMMWV Armament"] = 4,
          ["M1045 HMMWV TOW"] = 4,
          ["M1126 Stryker ICV"] = 9,
          ["M1134 Stryker ATGM"] = 9,
          ["Marder"] = 6,
          ["MCV-80"] = 9,
          ["MLRS FDDM"] = 4,
          ["MTLB"] = 25,
          ["TPZ"] = 10,
          ["Ural-4320 APA-5D"] = 10,
          ["GAZ-66"] = 8,
          ["GAZ-3307"] = 12,
          ["GAZ-3308"] = 14,
          ["Tigr_233036"] = 6,
          ["KAMAZ Truck"] = 12,
          ["KrAZ6322"] = 12,
          ["M 818"] = 12,
          ["Ural-375"] = 12,
          ["Ural-4320-31"] = 14,
          ["Ural-4320T"] = 14,
        }
    
        local CargoBayWeightLimit = ( Weights[Desc.typeName] or 0 ) * 95
        self.__.CargoBayWeightLimit = CargoBayWeightLimit
      end
    end
    self:F({CargoBayWeightLimit = 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 color to smoke to positionable.
-- @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


--- **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 @{Wrapper.Controllable#CONTROLLABLE.PushTask} or @{Wrapper.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.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.TaskLand}: (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 alititude.
--   * @{#CONTROLLABLE.TaskOrbitCircleAtVec2}: (AIR) Orbit at a specified position at a specified alititude during a specified duration with a specified speed.
--   * @{#CONTROLLABLE.TaskRefueling}: (AIR) Refueling from the nearest tanker. No parameters.
--   * @{#CONTROLLABLE.TaskRoute}: (AIR + GROUND) Return a Misson 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.
--   * @{#CONTROLLABLE.TaskReturnToBase}: (AIR) Route the controllable to an airbase.
--
-- ## 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 behaviour:
--
--   * @{#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_MISSIONS/tree/release-2-2-pre/GRP - Group Commands/GRP-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. 
-- 
-- # 5) Option methods
-- 
-- Controllable **Option methods** change the behaviour 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) Rule 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}
-- 
-- @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).
-- @return #nil The controllable is not existing or alive.  
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 behaviour! 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 behaviour! 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 behaviour! 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()
  self:F2()

  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 Wrapper.Controllable#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 Wrapper.Controllable#CONTROLLABLE self
function CONTROLLABLE:PushTask( DCSTask, WaitTime )
  self:F2()

  local DCSControllable = self:GetDCSObject()

  if DCSControllable then
    local Controller = self:_GetController()

    -- 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 )

    if WaitTime then
      self.TaskScheduler:Schedule( Controller, Controller.pushTask, { DCSTask }, WaitTime )
    else
      Controller:pushTask( DCSTask )
    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 Wrapper.Controllable#CONTROLLABLE self
function CONTROLLABLE:SetTask( DCSTask, WaitTime )
  self:F2( { DCSTask = DCSTask } )

  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.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 )
        --self:I( "After SetTask" )
      else
        BASE:E( { DCSControllableName .. " is not alive anymore.", DCSTask = DCSTask } )
      end
    end

    if not WaitTime or WaitTime == 0 then
      SetTask( self, 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 Wrapper.Controllable#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
-- @param #string userFlag
-- @param #boolean userFlagValue
-- @param #string condition
-- @param DCS#Time duration
-- @param #number lastWayPoint
-- return DCS#Task
function CONTROLLABLE:TaskCondition( time, userFlag, userFlagValue, condition, duration, lastWayPoint )
  self:F2( { 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

  self:T3( { DCSStopCondition } )
  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 )
  self:F2( { DCSTask, DCSStopCondition } )

  local DCSTaskControlled

  DCSTaskControlled = {
    id = 'ControlledTask',
    params = {
      task = DCSTask,
      stopCondition = DCSStopCondition
    }
  }

  self:T3( { DCSTaskControlled } )
  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 )
  self:F2( { DCSTasks } )

  local DCSTaskCombo

  DCSTaskCombo = {
    id = 'ComboTask',
    params = {
      tasks = DCSTasks
    }
  }
  
  for TaskID, Task in ipairs( DCSTasks ) do
    self:T( Task )
  end

  self:T3( { DCSTaskCombo } )
  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 )
  self:F2( { DCSCommand } )

  local DCSTaskWrappedAction
  
  DCSTaskWrappedAction = {
    id = "WrappedAction",
    enabled = true,
    number = Index or 1,
    auto = false,
    params = {
      action = DCSCommand,
    },
  }

  self:T3( { DCSTaskWrappedAction } )
  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
-- @param #CONTROLLABLE self
-- @param DCS#Command DCSCommand
-- @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

-- TASKS FOR AIR CONTROLLABLES


--- (AIR) Attack a Controllable.
-- @param #CONTROLLABLE self
-- @param Wrapper.Controllable#CONTROLLABLE AttackGroup The Controllable 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 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 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:TaskAttackGroup( AttackGroup, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit )
  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 DirectionEnabled = nil
  if Direction then
    DirectionEnabled = true
  end

  local AltitudeEnabled = nil
  if Altitude then
    AltitudeEnabled = true
  end

  local DCSTask
  DCSTask = { id = 'AttackGroup',
    params = {
      groupId = AttackGroup:GetID(),
      weaponType = WeaponType,
      expend = WeaponExpend,
      attackQty = AttackQty,
      directionEnabled = DirectionEnabled,
      direction = Direction,
      altitudeEnabled = AltitudeEnabled,
      altitude = Altitude,
      attackQtyLimit = AttackQtyLimit,
    },
  },

  self:T3( { DCSTask } )
  return DCSTask
end

--- (AIR) Attack the Unit.
-- @param #CONTROLLABLE self
-- @param Wrapper.Unit#UNIT AttackUnit The UNIT.
-- @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 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 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 #boolean Visible (optional) not a clue.
-- @param #number WeaponType (optional) The WeaponType.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskAttackUnit( AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType )
  self:F2( { self.ControllableName,          AttackUnit, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, WeaponType } )

  local DCSTask
  DCSTask = { 
    id = 'AttackUnit',
    params = {
      unitId = AttackUnit:GetID(),
      groupAttack = GroupAttack or false,
      visible = Visible or false,
      expend = WeaponExpend or "Auto",
      directionEnabled = Direction and true or false,
      direction = Direction,
      altitudeEnabled = Altitude and true or false,
      altitude = Altitude or 30,
      attackQtyLimit = AttackQty and true or false,
      attackQty = AttackQty,
      weaponType = WeaponType
    }
  }

  self:T3( DCSTask )
  
  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 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 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 )
  self:E( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType, Divebomb } )
  
  local _groupattack=false
  if GroupAttack then
    _groupattack=GroupAttack
  end
  
  local _direction=0
  local _directionenabled=false
  if Direction then
    _direction=math.rad(Direction)
    _directionenabled=true
  end
  
  local _altitude=5000
  local _altitudeenabled=false
  if Altitude then
    _altitude=Altitude
    _altitudeenabled=true
  end
  
  local _attacktype=nil
  if Divebomb then
    _attacktype="Dive"
  end
  

  local DCSTask
  DCSTask = { 
    id = 'Bombing',
    params = {
      x = Vec2.x,
      y = Vec2.y,
      groupAttack = _groupattack,
      expend = WeaponExpend or "Auto",
      attackQtyLimit = false, --AttackQty and true or false,
      attackQty = AttackQty or 1,
      directionEnabled = _directionenabled,
      direction = _direction, 
      altitudeEnabled = _altitudeenabled,
      altitude = _altitude,
      weaponType = WeaponType, 
      --attackType=_attacktype,
      },
  }

  self:E( { TaskBombing=DCSTask } )
  return DCSTask
end

--- (AIR) Attacking the map object (building, structure, e.t.c).
-- @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 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 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.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskAttackMapObject( Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType )
  self:F2( { self.ControllableName, Vec2, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, WeaponType } )

  local DCSTask
  DCSTask = { 
    id = 'AttackMapObject',
    params = {
      point = Vec2,
      groupAttack = GroupAttack or false,
      expend = WeaponExpend or "Auto",
      attackQtyLimit = AttackQty and true or false,
      attackQty = AttackQty, 
      directionEnabled = Direction and true or false,
      direction = Direction, 
      altitudeEnabled = Altitude and true or false,
      altitude = Altitude or 30,
      weaponType = WeaponType, 
    },
  },

  self:T3( { DCSTask } )
  return DCSTask
end


--- (AIR) Orbit at a specified position at a specified alititude 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 [m] 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 } )

  --  pattern = enum AI.Task.OribtPattern,
  --    point = Vec2,
  --    point2 = Vec2,
  --    speed = Distance,
  --    altitude = Distance

  local LandHeight = land.getHeight( Point )

  self:T3( { LandHeight } )

  local DCSTask = { id = 'Orbit',
    params = { pattern = AI.Task.OrbitPattern.CIRCLE,
      point = Point,
      speed = Speed,
      altitude = Altitude + LandHeight
    }
  }


  --  local AITask = { id = 'ControlledTask',
  --                   params = { task = { id = 'Orbit',
  --                                       params = { pattern = AI.Task.OrbitPattern.CIRCLE,
  --                                                  point = Point,
  --                                                  speed = Speed,
  --                                                  altitude = Altitude + LandHeight
  --                                                }
  --                                     },
  --                              stopCondition = { duration = Duration
  --                                              }
  --                            }
  --                 }
  --               )

  return DCSTask
end

--- (AIR) Orbit at the current position of the first unit of the controllable at a specified alititude.
-- @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 The coordinate where to orbit.
-- @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.
-- @param #CONTROLLABLE self
-- @param Wrapper.Airbase#AIRBASE Airbase Airbase to attack.
-- @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 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 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 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:TaskBombingRunway( Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack )
  self:F2( { self.ControllableName, Airbase, WeaponType, WeaponExpend, AttackQty, Direction, ControllableAttack } )

--  BombingRunway = { 
--    id = 'BombingRunway', 
--    params = { 
--      runwayId = AirdromeId,
--      weaponType = number, 
--      expend = enum AI.Task.WeaponExpend,
--      attackQty = number, 
--      direction = Azimuth, 
--      controllableAttack = boolean, 
--    } 
--  } 

  local DCSTask
  DCSTask = { id = 'BombingRunway',
    params = {
    point = Airbase:GetID(),
    weaponType = WeaponType, 
    expend = WeaponExpend,
    attackQty = AttackQty, 
    direction = Direction, 
    controllableAttack = ControllableAttack, 
    },
  },

  self:T3( { DCSTask } )
  return DCSTask
end


--- (AIR) Refueling from the nearest tanker. No parameters.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskRefueling()
  self:F2( { self.ControllableName } )

--  Refueling = { 
--    id = 'Refueling', 
--    params = {} 
--  }

  local DCSTask
  DCSTask = { id = 'Refueling',
    params = {
    },
  },

  self:T3( { DCSTask } )
  return DCSTask
end


--- (AIR HELICOPTER) Landing at the ground. For helicopters only.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Point The point where to land.
-- @param #number Duration The duration in seconds to stay on the ground.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskLandAtVec2( Point, Duration )
  self:F2( { self.ControllableName, Point, Duration } )

--  Land = {
--    id= 'Land',
--    params = {
--      point = Vec2,
--      durationFlag = boolean,
--      duration = Time
--    }
--  }
 
  local DCSTask
  if Duration and Duration > 0 then
    DCSTask = { id = 'Land', 
      params = { 
        point = Point, 
        durationFlag = true, 
        duration = Duration,
      }, 
    }
  else
    DCSTask = { id = 'Land', 
      params = { 
        point = Point, 
        durationFlag = false, 
      }, 
    }
  end

  self:T3( DCSTask )
  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.
-- @return #CONTROLLABLE self
function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint )
  self:F2( { self.ControllableName, Zone, Duration, RandomPoint } )

  local Point
  if RandomPoint then
    Point = Zone:GetRandomVec2()
  else
    Point = Zone:GetVec2()
  end

  local DCSTask = self:TaskLandAtVec2( Point, Duration )

  self:T3( DCSTask )
  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 Wrapper.Controllable#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
  if LastWaypointIndex then
    LastWaypointIndexFlag = true
  end
  
  local DCSTask
  DCSTask = { 
    id = 'Follow',
    params = {
      groupId = FollowControllable:GetID(),
      pos = Vec3,
      lastWptIndexFlag = LastWaypointIndexFlag,
      lastWptIndex = LastWaypointIndex
    }
  }

  self:T3( { DCSTask } )
  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 Wrapper.Controllable#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 Follow task is finished.
-- @param #number EngagementDistance Maximal distance from escorted controllable to threat. 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. 
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskEscort( FollowControllable, Vec3, LastWaypointIndex, EngagementDistance, TargetTypes )
  self:F2( { self.ControllableName, 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 LastWaypointIndexFlag = false
  if LastWaypointIndex then
    LastWaypointIndexFlag = true
  end
  
  TargetTypes=TargetTypes or {}
  
  local DCSTask
  DCSTask = { id = 'Escort',
    params = {
      groupId = FollowControllable:GetID(),
      pos = Vec3,
      lastWptIndexFlag = LastWaypointIndexFlag,
      lastWptIndex = LastWaypointIndex,
      engagementDistMax = EngagementDistance,
      targetTypes = TargetTypes,
    },
  },

  self:T3( { DCSTask } )
  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 
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFireAtPoint( Vec2, Radius, AmmoCount, WeaponType )
  self:F2( { self.ControllableName, Vec2, Radius, AmmoCount, WeaponType } )

  -- FireAtPoint = {
  --   id = 'FireAtPoint',
  --   params = {
  --     point = Vec2,
  --     radius = Distance,
  --     expendQty = number,
  --     expendQtyEnabled = boolean, 
  --   }
  -- }

  local DCSTask
  DCSTask = { id = 'FireAtPoint',
    params = {
      point = Vec2,
      radius = Radius,
      expendQty = 100, -- dummy value
      expendQtyEnabled = false,
    }
  }
  
  if AmmoCount then
    DCSTask.params.expendQty = AmmoCount
    DCSTask.params.expendQtyEnabled = true
  end
  
  if WeaponType then
    DCSTask.params.weaponType=WeaponType
  end

  self:T3( { DCSTask } )
  return DCSTask
end

--- (GROUND) Hold ground controllable from moving.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskHold()
  self:F2( { self.ControllableName } )

--  Hold = { 
--    id = 'Hold', 
--    params = { 
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'Hold',
    params = {
    }
  }

  self:T3( { DCSTask } )
  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. 
-- @param #CONTROLLABLE self
-- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE.
-- @param #number WeaponType Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. 
-- @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. 
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskFAC_AttackGroup( AttackGroup, WeaponType, Designation, Datalink )
  self:F2( { self.ControllableName, AttackGroup, WeaponType, Designation, Datalink } )

--  FAC_AttackGroup = { 
--    id = 'FAC_AttackGroup', 
--    params = { 
--      groupId = Group.ID,
--      weaponType = number,
--      designation = enum AI.Task.Designation,
--      datalink = boolean
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'FAC_AttackGroup',
    params = {
      groupId = AttackGroup:GetID(),
      weaponType = WeaponType,
      designation = Designation,
      datalink = Datalink,
    }
  }

  self:T3( { DCSTask } )
  return DCSTask
end

-- EN-ACT_ROUTE TASKS FOR AIRBORNE CONTROLLABLES

--- (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. 
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEngageTargets( Distance, TargetTypes, Priority )
  self:F2( { self.ControllableName, Distance, TargetTypes, Priority } )

--  EngageTargets ={ 
--    id = 'EngageTargets', 
--    params = { 
--      maxDist = Distance, 
--      targetTypes = array of AttributeName, 
--      priority = number 
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'EngageTargets',
    params = {
      maxDist = Distance, 
      targetTypes = TargetTypes, 
      priority = Priority 
    }
  }

  self:T3( { DCSTask } )
  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 Array of target categories allowed to engage. 
-- @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:EnRouteTaskEngageTargetsInZone( Vec2, Radius, TargetTypes, Priority )
  self:F2( { self.ControllableName, Vec2, Radius, TargetTypes, Priority } )

--  EngageTargetsInZone = { 
--    id = 'EngageTargetsInZone', 
--    params = { 
--      point = Vec2, 
--      zoneRadius = Distance, 
--      targetTypes = array of AttributeName,  
--      priority = number 
--    }
--  }

  local DCSTask
  DCSTask = { id = 'EngageTargetsInZone',
    params = {
      point = Vec2, 
      zoneRadius = Radius, 
      targetTypes = TargetTypes,  
      priority = Priority 
    }
  }

  self:T3( { DCSTask } )
  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.
-- @param #CONTROLLABLE self
-- @param Wrapper.Controllable#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 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 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 )
  self:F2( { self.ControllableName, AttackGroup, Priority, WeaponType, WeaponExpend, AttackQty, Direction, Altitude, AttackQtyLimit } )

  --  EngageControllable  = {
  --   id = 'EngageControllable ',
  --   params = {
  --     groupId = Group.ID,
  --     weaponType = number,
  --     expend = enum AI.Task.WeaponExpend,
  --     attackQty = number,
  --     directionEnabled = boolean,
  --     direction = Azimuth,
  --     altitudeEnabled = boolean,
  --     altitude = Distance,
  --     attackQtyLimit = boolean,
  --     priority = number,
  --   }
  -- }

  local DirectionEnabled = nil
  if Direction then
    DirectionEnabled = true
  end

  local AltitudeEnabled = nil
  if Altitude then
    AltitudeEnabled = true
  end

  local DCSTask
  DCSTask = { id = 'EngageControllable',
    params = {
      groupId = AttackGroup:GetID(),
      weaponType = WeaponType,
      expend = WeaponExpend,
      attackQty = AttackQty,
      directionEnabled = DirectionEnabled,
      direction = Direction,
      altitudeEnabled = AltitudeEnabled,
      altitude = Altitude,
      attackQtyLimit = AttackQtyLimit,
      priority = Priority,
    },
  },

  self:T3( { DCSTask } )
  return DCSTask
end


--- (AIR) Search and attack the Unit.
-- @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 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 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 )
  self:F2( { self.ControllableName,          EngageUnit, Priority, GroupAttack, WeaponExpend, AttackQty, Direction, Altitude, Visible, ControllableAttack } )

  --  EngageUnit = {
  --    id = 'EngageUnit',
  --    params = {
  --      unitId = Unit.ID,
  --      weaponType = number,
  --      expend = enum AI.Task.WeaponExpend
  --      attackQty = number,
  --      direction = Azimuth,
  --      attackQtyLimit = boolean,
  --      controllableAttack = boolean,
  --      priority = number,
  --    }
  --  }

  local DCSTask
  DCSTask = { id = 'EngageUnit',
    params = {
      unitId = EngageUnit:GetID(),
      priority = Priority or 1,
      groupAttack = GroupAttack or false,
      visible = Visible or false,
      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,
      controllableAttack = ControllableAttack,
    },
  },

  self:T3( { DCSTask } )
  return DCSTask
end



--- (AIR) Aircraft will act as an AWACS for friendly units (will provide them with information about contacts). No parameters.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskAWACS( )
  self:F2( { self.ControllableName } )

--  AWACS = { 
--    id = 'AWACS', 
--    params = { 
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'AWACS',
    params = {
    }
  }

  self:T3( { DCSTask } )
  return DCSTask
end


--- (AIR) Aircraft will act as a tanker for friendly units. No parameters.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskTanker( )
  self:F2( { self.ControllableName } )

--  Tanker = { 
--    id = 'Tanker', 
--    params = { 
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'Tanker',
    params = {
    }
  }

  self:T3( { DCSTask } )
  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.
-- @param #CONTROLLABLE self
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskEWR( )
  self:F2( { self.ControllableName } )

--  EWR = { 
--    id = 'EWR', 
--    params = { 
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'EWR',
    params = {
    }
  }

  self:T3( { DCSTask } )
  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. 
-- @param #CONTROLLABLE self
-- @param Wrapper.Controllable#CONTROLLABLE AttackGroup Target CONTROLLABLE.
-- @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 Bitmask of weapon types those allowed to use. If parameter is not defined that means no limits on weapon usage. 
-- @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. 
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:EnRouteTaskFAC_EngageGroup( AttackGroup, Priority, WeaponType, Designation, Datalink )
  self:F2( { self.ControllableName, AttackGroup, WeaponType, Priority, Designation, Datalink } )

--  FAC_EngageControllable  = { 
--    id = 'FAC_EngageControllable', 
--    params = { 
--      groupId = Group.ID,
--      weaponType = number,
--      designation = enum AI.Task.Designation,
--      datalink = boolean,
--      priority = number,
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'FAC_EngageControllable',
    params = {
      groupId = AttackGroup:GetID(),
      weaponType = WeaponType,
      designation = Designation,
      datalink = Datalink,
      priority = Priority,
    }
  }

  self:T3( { DCSTask } )
  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. 
-- 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. 
-- @param #CONTROLLABLE self
-- @param DCS#Distance Radius  The maximal distance from the FAC to a target.
-- @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( Radius, Priority )
  self:F2( { self.ControllableName, Radius, Priority } )

--  FAC = { 
--    id = 'FAC', 
--    params = { 
--      radius = Distance,
--      priority = number
--    } 
--  }

  local DCSTask
  DCSTask = { id = 'FAC',
    params = {
      radius = Radius,
      priority = Priority
    }
  }

  self:T3( { DCSTask } )
  return DCSTask
end




--- (AIR) Move the controllable to a Vec2 Point, wait for a defined duration and embark a controllable.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Point The point where to wait.
-- @param #number Duration The duration in seconds to wait.
-- @param #CONTROLLABLE EmbarkingControllable The controllable to be embarked.
-- @return DCS#Task The DCS task structure
function CONTROLLABLE:TaskEmbarking( Point, Duration, EmbarkingControllable )
  self:F2( { self.ControllableName, Point, Duration, EmbarkingControllable.DCSControllable } )

  local DCSTask
  DCSTask =  { id = 'Embarking',
    params = { x = Point.x,
      y = Point.y,
      duration = Duration,
      controllablesForEmbarking = { EmbarkingControllable.ControllableID },
      durationFlag = true,
      distributionFlag = false,
      distribution = {},
    }
  }

  self:T3( { DCSTask } )
  return DCSTask
end

--- (GROUND) Embark to a Transport landed at a location.

--- Move to a defined Vec2 Point, and embark to a controllable when arrived within a defined Radius.
-- @param #CONTROLLABLE self
-- @param DCS#Vec2 Point The point where to wait.
-- @param #number Radius The radius of the embarking zone around the Point.
-- @return DCS#Task The DCS task structure.
function CONTROLLABLE:TaskEmbarkToTransport( Point, Radius )
  self:F2( { self.ControllableName, Point, Radius } )

  local DCSTask --DCS#Task
  DCSTask = { id = 'EmbarkToTransport',
    params = { x = Point.x,
      y = Point.y,
      zoneRadius = Radius,
    }
  }

  self:T3( { DCSTask } )
  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, ... )

  local DCSTask

  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

  DCSTask = self:TaskWrappedAction(self:CommandDoScript(table.concat( DCSScript )))

  self:T( DCSTask )

  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 )
  self:F2( Points )

  local DCSTask
  DCSTask = { id = 'Mission', params = { TaskMission, }, }

  self:T3( { DCSTask } )
  return DCSTask
end


do -- Patrol methods

  --- (GROUND) Patrol iteratively using the waypoints the for 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()
      local From = FromCoord:WaypointGround( 120 )
      
      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 ) -- 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
      
      -- Loop until a waypoint has been found that is not the same as the current waypoint.
      -- Otherwise the object zon'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 = {}
      Route[#Route+1] = FromCoord:WaypointGround( 0 )
      Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation )
      
      
      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.
  -- @return #CONTROLLABLE
  function CONTROLLABLE:PatrolZones( ZoneList, Speed, Formation )
  
    if not 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

    self:F( { PatrolGroup = PatrolGroup:GetName() } )
    
    if PatrolGroup:IsGround() or PatrolGroup:IsShip() then
    
      local Waypoints = PatrolGroup:GetTemplateRoutePoints()
      local Waypoint = Waypoints[math.random( 1, #Waypoints )] -- Select random waypoint.
      
      -- Calculate the new Route.
      local FromCoord = PatrolGroup: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:GetRandomCoordinate( 10 )
      
      -- Create a "ground route point", which is a "point" structure that can be given as a parameter to a Task
      local Route = {}
      Route[#Route+1] = FromCoord:WaypointGround( 20 )
      Route[#Route+1] = ToCoord:WaypointGround( Speed, Formation )
      
      
      local TaskRouteToZone = PatrolGroup:TaskFunction( "CONTROLLABLE.PatrolZones", ZoneList, Speed, Formation )
      
      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

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
function CONTROLLABLE:TaskRoute( Points )
  self:F2( Points )

  local DCSTask
  DCSTask = { id = 'Mission', params = { route = { points = Points, }, }, }

  self:T3( { DCSTask } )
  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.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteGroundTo( ToCoordinate, Speed, Formation, DelaySeconds )
  
    local FromCoordinate = self:GetCoordinate()
    
    local FromWP = FromCoordinate:WaypointGround()
    local ToWP = ToCoordinate:WaypointGround( Speed, Formation )
  
    self:Route( { FromWP, ToWP }, 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".
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteGroundOnRoad( ToCoordinate, Speed, DelaySeconds, OffRoadFormation )
  
    -- 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)
    
    -- 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.
  -- @return #CONTROLLABLE The CONTROLLABLE.
  function CONTROLLABLE:RouteGroundOnRailRoads( ToCoordinate, Speed, DelaySeconds)
  
    -- Defaults.
    Speed=Speed or 20
    DelaySeconds=DelaySeconds or 1
  
    -- Get the route task.
    local route=self:TaskGroundOnRailRoads(ToCoordinate, Speed)
    
    -- 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.
  -- @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 )
    self:F2({ToCoordinate=ToCoordinate, Speed=Speed, OffRoadFormation=OffRoadFormation})
    
    -- 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=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 PathOnRoad 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 PathOnRoad 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

    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.
  -- @return Task
  function CONTROLLABLE:TaskGroundOnRailRoads(ToCoordinate, Speed)
    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

    return route 
  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 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 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 routines.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 routines.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] = routines.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 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 Wrapper.Controllable#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.OPTICAL 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
    
    self:T2( { DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK } )
    
    return self:_GetController():getDetectedTargets( DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK )
  end

  return nil
end

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.OPTICAL 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, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity
      = Controller:isTargetDetected( DCSObject, DetectionVisual, DetectionOptical, DetectionRadar, DetectionIRST, DetectionRWR, DetectionDLINK )
      
    return TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, TargetLastPos, TargetLastVelocity
  end

  return nil
end

-- Options

--- 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

--- Holding weapons.
-- @param Wrapper.Controllable#CONTROLLABLE self
-- @return Wrapper.Controllable#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.
-- @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

--- Openfire.
-- @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 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

--- 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

--- 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)
    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 )
    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)
    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

  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.GROUND.id.RTB_ON_OUT_OF_AMMO, WeaponsFlag )
    end

    return self
  end

  return nil
end





--- Retrieve the controllable mission and allow to place function hooks within the mission waypoint plan.
-- Use the method @{Wrapper.Controllable#CONTROLLABLE:WayPointFunction} to define the hook functions for specific waypoints.
-- Use the method @{Controllable@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
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
function CONTROLLABLE:WayPointFunction( WayPoint, WayPointIndex, WayPointFunction, ... )
  self:F2( { WayPoint, WayPointIndex, WayPointFunction } )

  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
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



-- Message APIs--- **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 SANATIZE 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 _@{DATABASE} object.
-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Group objects are spawned (using the @{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.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
--   * [**Entropy**](https://forums.eagle.ru/member.php?u=111471), **Afinegan**: Came up with the requirement for AIOnOff().
-- 
-- ===
-- 
-- @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.
-- 
-- The GROUP class provides the following functions to retrieve quickly the relevant GROUP instance:
--
--  * @{#GROUP.Find}(): Find a GROUP instance from the _DATABASE object using a DCS Group object.
--  * @{#GROUP.FindByName}(): Find a GROUP instance from the _DATABASE object using a DCS Group name.
--
-- # 1. 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 @{Zone}.
-- Use the following Zone validation methods on the group:
-- 
--   * @{#GROUP.IsCompletelyInZone}: Returns true if all units of the group are within a @{Zone}.
--   * @{#GROUP.IsPartlyInZone}: Returns true if some units of the group are within a @{Zone}.
--   * @{#GROUP.IsNotInZone}: Returns true if none of the group units of the group are within a @{Zone}.
--   
-- The zone can be of any @{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" }
}

--- 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 the created 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

-- DCS Group methods support.

--- Returns the DCS Group.
-- @param #GROUP self
-- @return DCS#Group The DCS Group.
function GROUP:GetDCSObject()
  local DCSGroup = Group.getByName( self.GroupName )

  if DCSGroup then
    return DCSGroup
  end

  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.
-- @return #nil The POSITIONABLE is not existing or alive.  
function GROUP:GetPositionVec3() -- Overridden from POSITIONABLE:GetPositionVec3()
  self:F2( self.PositionableName )

  local DCSPositionable = self:GetDCSObject()
  
  if DCSPositionable then
    local PositionablePosition = DCSPositionable:getUnits()[1]:getPosition().p
    self:T3( PositionablePosition )
    return PositionablePosition
  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.
-- @return #boolean false if the group is alive but inactive.
-- @return #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.
-- @return #nil The group is not existing or alive.  
function GROUP:IsActive()
  self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject() -- DCS#Group
  
  if DCSGroup then
  
    local GroupIsActive = DCSGroup:getUnit(1):isActive()
    return GroupIsActive 
  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 true if you want to generate a crash or dead event for each unit.
-- @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 )
  self:F2( self.GroupName )

  local DCSGroup = self:GetDCSObject()

  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 )
        else
          self:CreateEventDead( timer.getTime(), UnitData )
        end
      elseif GenerateEvent == false then
        -- Do nothing!
      else
        self:CreateEventRemoveUnit( timer.getTime(), UnitData )
      end
    end
    USERFLAG:New( self:GetName() ):Set( 100 )
    DCSGroup:destroy()
    DCSGroup = nil
  end

  return nil
end


--- Returns category of the DCS Group.
-- @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
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",
    }
    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 )

  local DCSGroup = self:GetDCSObject()
  if DCSGroup then
    local GroupCoalition = DCSGroup:getCoalition()
    self:T3( GroupCoalition )
    return GroupCoalition
  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()
  
  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

--- 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 then
        speedmax=speed
      elseif speed<speedmax then
        speedmax=speed
      end
    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 #list<Wrapper.Unit#UNIT> The list of @{Wrapper.Unit} objects of the @{Wrapper.Group}.
function GROUP:GetUnits()
  self:F2( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnits = DCSGroup:getUnits()
    local Units = {}
    for Index, UnitData in pairs( DCSUnits ) do
      Units[#Units+1] = UNIT:Find( UnitData )
    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


--- Returns the UNIT wrapper class with number UnitNumber.
-- If the underlying DCS Unit does not 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 wrapper class.
function GROUP:GetUnit( UnitNumber )
  self:F3( { self.GroupName, UnitNumber } )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnit = DCSGroup:getUnit( UnitNumber )
    local UnitFound = UNIT:Find( DCSGroup:getUnit( UnitNumber ) )
    self:T2( UnitFound )
    return UnitFound
  end

  return nil
end

--- Returns the DCS Unit with number UnitNumber.
-- If the underlying DCS Unit does not exist, the method will return nil. .
-- @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 )
  self:F3( { self.GroupName, UnitNumber } )

  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local DCSUnitFound = DCSGroup:getUnit( UnitNumber )
    self:T3( DCSUnitFound )
    return DCSUnitFound
  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()
  self:F3( { self.GroupName } )
  local DCSGroup = self:GetDCSObject()

  if DCSGroup then
    local GroupSize = DCSGroup:getSize()
    
    if GroupSize then
      self:T3( GroupSize )
      return GroupSize
    else
      return 0
    end
  end

  return nil
end


--- Returns the average velocity Vec3 vector.
-- @param Wrapper.Group#GROUP self
-- @return DCS#Vec3 The velocity Vec3 vector
-- @return #nil 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 = #GroupUnits
    
    local VelocityVec3 = { x = 0, y = 0, z = 0 }
    
    for _, DCSUnit in pairs( GroupUnits ) do
      local UnitVelocityVec3 = DCSUnit:getVelocity()
      VelocityVec3.x = VelocityVec3.x + UnitVelocityVec3.x
      VelocityVec3.y = VelocityVec3.y + UnitVelocityVec3.y
      VelocityVec3.z = VelocityVec3.z + UnitVelocityVec3.z
    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 height in meters.
-- @param Wrapper.Group#GROUP self
-- @param #boolean FromGround Measure from the ground or from sea level. Provide **true** for measuring from the ground. **false** or **nil** if you measure from sea level. 
-- @return DCS#Vec3 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
-- @return #GROUP self
function GROUP:Activate()
  self:F2( { self.GroupName } )
  trigger.action.activateGroup( self:GetDCSObject() )
  return self:GetDCSObject()
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

--- 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()
  self:F2( self.GroupName )

  local UnitPoint = self:GetUnit(1)
  UnitPoint:GetVec2()
  local GroupPointVec2 = UnitPoint:GetVec2()
  self:T3( GroupPointVec2 )
  return GroupPointVec2
end

--- Returns the current Vec3 vector of the first DCS Unit in the GROUP.
-- @param #GROUP self
-- @return DCS#Vec3 Current Vec3 of the first DCS Unit of the GROUP.
function GROUP:GetVec3()
  self:F2( self.GroupName )

  local GroupVec3 = self:GetUnit(1):GetVec3()
  self:T3( GroupVec3 )
  return GroupVec3
end

--- Returns a POINT_VEC2 object indicating the point in 2D of the first UNIT of the GROUP within the mission.
-- @param #GROUP self
-- @return Core.Point#POINT_VEC2 The 2D 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 GetPointVec2", Group = self, Alive = self:IsAlive() } )

  return nil
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()
  self:F2( self.PositionableName )

  local FirstUnit = self:GetUnit(1)
  
  if FirstUnit then
    local FirstUnitCoordinate = FirstUnit:GetCoordinate()
    self:T3(FirstUnitCoordinate)
    return FirstUnitCoordinate
  end
  
  BASE:E( { "Cannot GetCoordinate", Group = self, Alive = self:IsAlive() } )

  return nil
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
-- @return DCS#Vec3 The random 3D point vector around the first UNIT of the GROUP.
-- @return #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
-- @return #nil The first UNIT is not existing or alive.
function GROUP:GetHeading()
  self:F2(self.GroupName)

  local GroupSize = self:GetSize()
  local HeadingAccumulator = 0
  
  if GroupSize then
    for i = 1, GroupSize do
      HeadingAccumulator = HeadingAccumulator + self:GetUnit(i):GetHeading()
    end
    return math.floor(HeadingAccumulator / GroupSize)
  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 #Unit reference to #Unit object for further processing
function GROUP:GetFuelMin()
  self:F(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
    tmp = UnitData:GetFuel()
    if tmp < min then
      min = tmp
      unit = UnitData
    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()
      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


do -- Is Zone methods

--- Returns true if all units of the group are within a @{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 @{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 @{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 @{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 @{Zone}
-- @param #GROUP self
-- @param Core.Zone#ZONE_BASE Zone The zone to test.
-- @return #number The number of UNITs that are in the @{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

      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

    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.
-- 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()

      if UnitHeight > GroupHeightMax then
        GroupHeightMax = UnitHeight
      end
    end

    return GroupHeightMax
  end

  return nil
end

-- RESPAWNING

--- Returns the group template from the @{DATABASE} (_DATABASE object).
-- @param #GROUP self
-- @return #table 
function GROUP:GetTemplate()
  local GroupName = self:GetName()
  return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ) )
end

--- Returns the group template route.points[] (the waypoints) from the @{DATABASE} (_DATABASE object).
-- @param #GROUP self
-- @return #table 
function GROUP:GetTemplateRoutePoints()
  local GroupName = self:GetName()
  return UTILS.DeepCopy( _DATABASE:GetGroupTemplate( GroupName ).route.points )
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 @{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 @{Zone}.
-- When a Respawn happens, the units of the group will be placed at random positions within the Zone (selected).
-- @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


--- Respawn the @{Wrapper.Group} at a @{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 @{Zone} for the respawned group.
--   - @{#GROUP.InitRandomizeZones}: Randomize the respawn @{Zone} between one of the @{Zone}s given for the respawned group.
--   - @{#GROUP.InitRandomizePositionZone}: Randomize the positions of the units of the respawned group within the @{Zone}.
--   - @{#GROUP.InitRandomizePositionRadius}: Randomize the positions of the units of the respawned group in a circle band.
--   - @{#GROUP.InitRandomizeTemplates}: Randomize the Template for the respawned group.
-- 
-- 
-- Notes:
-- 
--   - When InitZone or InitRandomizeZones is not used, the position of the respawned group will be its current position.
--   - 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.
function GROUP:Respawn( Template, Reset )

  if not Template then
    Template = self:GetTemplate()
  end

  if self:IsAlive() then
    local Zone = self.InitRespawnZone -- Core.Zone#ZONE
    local Vec3 = Zone and Zone:GetVec3() or self:GetVec3()
    local From = { x = Template.x, y = Template.y }
    Template.x = Vec3.x
    Template.y = Vec3.z
    --Template.x = nil
    --Template.y = nil
    
    self:F( #Template.units )
    if Reset == true then
      for UnitID, UnitData in pairs( self:GetUnits() ) do
        local GroupUnit = UnitData -- Wrapper.Unit#UNIT
        self:F( GroupUnit:GetName() )
        if GroupUnit:IsAlive() then
          self:F( "Alive"  )
          local GroupUnitVec3 = GroupUnit:GetVec3() 
          if Zone then
            if self.InitRespawnRandomizePositionZone then
              GroupUnitVec3 = Zone:GetRandomVec3()
            else
              if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then
                GroupUnitVec3 = POINT_VEC3:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner )
              else
                GroupUnitVec3 = Zone:GetVec3()
              end
            end
          end
          
          Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y
          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.
          Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or GroupUnit:GetHeading()
          self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } )
        end
      end
    else
      for UnitID, TemplateUnitData in pairs( Template.units ) do
        self:F( "Reset"  )
        local GroupUnitVec3 = { x = TemplateUnitData.x, y = TemplateUnitData.alt, z = TemplateUnitData.y }
        if Zone then
          if self.InitRespawnRandomizePositionZone then
            GroupUnitVec3 = Zone:GetRandomVec3()
          else
            if self.InitRespawnRandomizePositionInner and self.InitRespawnRandomizePositionOuter then
              GroupUnitVec3 = POINT_VEC3:NewFromVec2( From ):GetRandomPointVec3InRadius( self.InitRespawnRandomizePositionsOuter, self.InitRespawnRandomizePositionsInner )
            else
              GroupUnitVec3 = Zone:GetVec3()
            end
          end
        end
        
        Template.units[UnitID].alt = self.InitRespawnHeight and self.InitRespawnHeight or GroupUnitVec3.y
        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.
        Template.units[UnitID].heading = self.InitRespawnHeading and self.InitRespawnHeading or TemplateUnitData.heading
        self:F( { UnitID, Template.units[UnitID], Template.units[UnitID] } )
      end
    end      
    
  end
  
  self:Destroy()
  _DATABASE:Spawn( Template )
  
  self:ResetEvents()
  
  return self
  
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} )

  -- 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:GetDesc().category
    
    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

    -- Destroy old group.
    self:Destroy(false)
    
    _DATABASE:Spawn( SpawnTemplate )
  
    -- Reset events.
    self:ResetEvents()

    return self
  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 routines.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 )

  return routines.utils.deepCopy( _DATABASE.Templates.Groups[self.GroupName].Template.route.points )
end

--- Return the route of a group by using the @{Core.Database#DATABASE} class.
-- @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] = routines.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
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

--- 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

--- 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

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, the maximum Speed of the first unit is selected. 
  -- @return #GROUP
  function GROUP:RouteRTB( RTBAirbase, Speed )
    self:F( { RTBAirbase:GetName(), Speed } )
  
    local DCSGroup = self:GetDCSObject()
  
    if DCSGroup then
  
      if RTBAirbase then
      
        local GroupPoint = self:GetVec2()
        local GroupVelocity = self:GetUnit(1):GetDesc().speedMax
    
        local PointFrom = {}
        PointFrom.x = GroupPoint.x
        PointFrom.y = GroupPoint.y
        PointFrom.type = "Turning Point"
        PointFrom.action = "Turning Point"
        PointFrom.speed = GroupVelocity

    
        local PointTo = {}
        local AirbasePointVec2 = RTBAirbase:GetPointVec2()
        local AirbaseAirPoint = AirbasePointVec2:WaypointAir(
          POINT_VEC3.RoutePointAltType.BARO,
          "Land",
          "Landing", 
          Speed or self:GetUnit(1):GetDesc().speedMax
        )
        
        AirbaseAirPoint["airdromeId"] = RTBAirbase:GetID()
        AirbaseAirPoint["speed_locked"] = true,
    
        self:F(AirbaseAirPoint )
    
        local Points = { PointFrom, AirbaseAirPoint }
    
        self:T3( Points )

        local Template = self:GetTemplate()
        Template.route.points = Points
        self:Respawn( Template )
    
        --self:Route( Points )
      else
        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() ) 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 ) 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

--do -- Smoke
--
----- Signal a flare at the position of the GROUP.
---- @param #GROUP self
---- @param Utilities.Utils#FLARECOLOR FlareColor
--function GROUP:Flare( FlareColor )
--  self:F2()
--  trigger.action.signalFlare( self:GetVec3(), FlareColor , 0 )
--end
--
----- Signal a white flare at the position of the GROUP.
---- @param #GROUP self
--function GROUP:FlareWhite()
--  self:F2()
--  trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.White , 0 )
--end
--
----- Signal a yellow flare at the position of the GROUP.
---- @param #GROUP self
--function GROUP:FlareYellow()
--  self:F2()
--  trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Yellow , 0 )
--end
--
----- Signal a green flare at the position of the GROUP.
---- @param #GROUP self
--function GROUP:FlareGreen()
--  self:F2()
--  trigger.action.signalFlare( self:GetVec3(), trigger.flareColor.Green , 0 )
--end
--
----- Signal a red flare at the position of the GROUP.
---- @param #GROUP self
--function GROUP:FlareRed()
--  self:F2()
--  local Vec3 = self:GetVec3()
--  if Vec3 then
--    trigger.action.signalFlare( Vec3, trigger.flareColor.Red, 0 )
--  end
--end
--
----- Smoke the GROUP.
---- @param #GROUP self
--function GROUP:Smoke( SmokeColor, Range )
--  self:F2()
--  if Range then
--    trigger.action.smoke( self:GetRandomVec3( Range ), SmokeColor )
--  else
--    trigger.action.smoke( self:GetVec3(), SmokeColor )
--  end
--  
--end
--
----- Smoke the GROUP Green.
---- @param #GROUP self
--function GROUP:SmokeGreen()
--  self:F2()
--  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Green )
--end
--
----- Smoke the GROUP Red.
---- @param #GROUP self
--function GROUP:SmokeRed()
--  self:F2()
--  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Red )
--end
--
----- Smoke the GROUP White.
---- @param #GROUP self
--function GROUP:SmokeWhite()
--  self:F2()
--  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.White )
--end
--
----- Smoke the GROUP Orange.
---- @param #GROUP self
--function GROUP:SmokeOrange()
--  self:F2()
--  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Orange )
--end
--
----- Smoke the GROUP Blue.
---- @param #GROUP self
--function GROUP:SmokeBlue()
--  self:F2()
--  trigger.action.smoke( self:GetVec3(), trigger.smokeColor.Blue )
--end
--
--
--
--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: 
-- 
-- ===
-- 
-- @module Wrapper.Unit
-- @image Wrapper_Unit.JPG


--- @type UNIT
-- @extends Wrapper.Controllable#CONTROLLABLE

--- For each DCS Unit object alive within a running mission, a UNIT wrapper object (instance) will be created within the _@{DATABASE} object.
-- This is done at the beginning of the mission (when the mission starts), and dynamically when new DCS Unit objects are spawned (using the @{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 _DATABASE object using a DCS Unit object.
--  * @{#UNIT.FindByName}(): Find a UNIT instance from the _DATABASE object using a DCS Unit name.
--  
-- IMPORTANT: ONE SHOULD NEVER SANATIZE 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 ori�ntation 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
UNIT = {
	ClassName="UNIT",
}


--- 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
function UNIT:Register( UnitName )
  local self = BASE:Inherit( self, CONTROLLABLE:New( UnitName ) )
  self.UnitName = UnitName
  
  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

--- Return the name of the UNIT.
-- @param #UNIT self
-- @return #string The UNIT name.
function UNIT:Name()
  
  return self.UnitName
end


--- @param #UNIT self
-- @return DCS#Unit
function UNIT:GetDCSObject()

  local DCSUnit = Unit.getByName( self.UnitName )

  if DCSUnit then
    return DCSUnit
  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() ) 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 )

  _DATABASE:Spawn( SpawnGroupTemplate )
end



--- Returns if the unit is activated.
-- @param #UNIT self
-- @return #boolean true if Unit is activated.
-- @return #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 alive.  
-- If the Unit is not alive, 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 true if Unit is alive and active.
-- @return #boolean false if Unit is alive but not active.
-- @return #nil if the Unit is not existing or is not alive.  
function UNIT:IsAlive()
  self:F3( self.UnitName )

  local DCSUnit = self:GetDCSObject() -- DCS#Unit
  
  if DCSUnit then
    local UnitIsAlive  = DCSUnit:isExist() and DCSUnit:isActive()
    return UnitIsAlive
  end 
  
  return nil
end



--- Returns the Unit's callsign - the localized string.
-- @param #UNIT self
-- @return #string The Callsign of the Unit.
-- @return #nil The DCS Unit is not existing or alive.  
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


--- 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:F2( 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

--- 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
    return SpeedMax*3.6
  end

  return nil
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 nautical miles for some reason. But should check again!
    if Range then
      Range=UTILS.NMToMeters(Range)
    else
      Range=10000000 --10.000 km if no range
    end
    return Range
  end

  return nil
end

--- Returns the unit's group if it exist and nil otherwise.
-- @param Wrapper.Unit#UNIT self
-- @return Wrapper.Group#GROUP The Group of the Unit.
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetGroup()
  self:F2( self.UnitName )

  local DCSUnit = self:GetDCSObject()
  
  if DCSUnit then
    local UnitGroup = GROUP:FindByName( DCSUnit:getGroup():getName() )
    return UnitGroup
  end

  return nil
end


-- Need to add here functions to check if radar is on and which object etc.

--- 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 @{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
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetAmmo()
  self:F2( self.UnitName )

  local DCSUnit = self:GetDCSObject()
  
  if DCSUnit then
    local UnitAmmo = DCSUnit:getAmmo()
    return UnitAmmo
  end
  
  return nil
end

--- Returns the unit sensors.
-- @param #UNIT self
-- @return DCS#Unit.Sensors
-- @return #nil The DCS Unit is not existing or alive.  
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. 
-- @return #nil The DCS Unit is not existing or alive.  
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. 
-- @return #nil The DCS Unit is not existing or alive.  
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 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.
-- @return #nil The DCS Unit is not existing or alive.  
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).
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetFuel()
  self:F( 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:F2( { self.UnitName } )
  local DCSUnit = self:GetDCSObject()

  local Units = {}
  
  if DCSUnit then
    Units[1] = UNIT:Find( DCSUnit )
    self:T3( Units )
    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.
-- @return #nil The DCS Unit is not existing or alive.  
function UNIT:GetLife()
  self:F2( self.UnitName )

  local DCSUnit = self:GetDCSObject()
  
  if DCSUnit 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.
-- @return #nil The DCS Unit is not existing or alive.  
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 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 ...
-- The following threat levels are foreseen:
-- 
--   * 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.
--   @param #UNIT self
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 = {
        "Unarmed", 
        "Infantry", 
        "Old Tanks & APCs", 
        "Tanks & IFVs without ATGM",   
        "Tanks & IFV with ATGM",
        "Modern Tanks",
        "AAA",
        "IR Guided SAMs",
        "SR SAMs",
        "MR SAMs",
        "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"]                                                   then ThreatLevel = 1
      end
      
      ThreatText = ThreatLevels[ThreatLevel+1]
    end
    
    if self:IsAir() then
    
      local ThreatLevels = {
        "Unarmed", 
        "Tanker", 
        "AWACS", 
        "Transport Helicopter",   
        "UAV",
        "Bomber",
        "Strategic Bomber",
        "Attack Helicopter",
        "Battleplane",
        "Multirole Fighter",
        "Fighter"
      }
      
      
      if     Attributes["Fighters"]                                 then ThreatLevel = 10
      elseif Attributes["Multirole fighters"]                       then ThreatLevel = 9
      elseif Attributes["Battleplanes"]                             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 = {
        "Unarmed ship", 
        "Light armed ships", 
        "Corvettes",
        "",
        "Frigates",
        "",
        "Cruiser",
        "",
        "Destroyer",
        "",
        "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


-- Is functions

--- Returns true if the unit is within a @{Zone}.
-- @param #UNIT 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 UNIT:IsInZone( Zone )
  self:F2( { self.UnitName, 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 @{Zone}.
-- @param #UNIT 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 UNIT:IsNotInZone( Zone )
  self:F2( { self.UnitName, Zone } )

  if self:IsAlive() then
    local IsInZone = not Zone:IsVec3InZone( self:GetVec3() )
    
    self:T( { IsInZone } )
    return IsInZone 
  else
    return false
  end
end


--- 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
-- @return #boolean true if in the air.
-- @return #nil The UNIT is not existing or alive.  
function UNIT:InAir()
  self:F2( self.UnitName )

  local DCSUnit = self:GetDCSObject() --DCS#Unit
  
  if DCSUnit then
--    Implementation of workaround. The original code is below.
--    This to simulate the landing on buildings.

    local UnitInAir = true

    local UnitCategory = DCSUnit:getDesc().category
    if UnitCategory == Unit.Category.HELICOPTER then
      local VelocityVec3 = DCSUnit:getVelocity()
      local Velocity = ( VelocityVec3.x ^ 2 + VelocityVec3.y ^ 2 + VelocityVec3.z ^ 2 ) ^ 0.5 -- in meters / sec
      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
    else
      UnitInAir = DCSUnit:inAir()
    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 Event
  -- @param #function EventFunction (optional) The function to be called when the event occurs for the unit.
  -- @return #UNIT
  function UNIT:HandleEvent( Event, EventFunction )
  
    self:EventDispatcher():OnEventForUnit( self:GetName(), EventFunction, self, Event )
    
    return self
  end
  
  --- UnSubscribe to a DCS event.
  -- @param #UNIT self
  -- @param Core.Event#EVENTS Event
  -- @return #UNIT
  function UNIT:UnHandleEvent( Event )
  
    self:EventDispatcher():RemoveForUnit( self:GetName(), self, Event )
    
    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
  

end--- **Wrapper** -- CLIENT wraps DCS Unit objects acting as a __Client__ or __Player__ within a mission.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Wrapper.Client
-- @image Wrapper_Client.JPG


--- The CLIENT class
-- @type CLIENT
-- @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 @{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 _@{DATABASE} object.
-- 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 _DATABASE object using a DCS Unit object.
--  * @{#CLIENT.FindByName}(): Find a CLIENT instance from the _DATABASE object using a DCS Unit name.
--  
-- **IMPORTANT: ONE SHOULD NEVER SANATIZE these CLIENT OBJECT REFERENCES! (make the CLIENT object references nil).**
-- 
-- @field #CLIENT
CLIENT = {
	ONBOARDSIDE = {
		NONE = 0,
		LEFT = 1,
		RIGHT = 2,
		BACK = 3,
		FRONT = 4
	},
	ClassName = "CLIENT",
	ClientName = nil,
	ClientAlive = false,
	ClientTransport = false,
	ClientBriefingShown = false,
	_Menus = {},
	_Tasks = {},
	Messages = { 
	}
}


--- Finds a CLIENT from the _DATABASE using the relevant DCS Unit.
-- @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.
-- @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: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 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 )
  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

function CLIENT:Register( ClientName )
  local self = BASE:Inherit( self, UNIT:Register( ClientName ) )

  self:F( ClientName )
  self.ClientName = ClientName
  self.MessageSwitch = true
  self.ClientAlive2 = false
  
  --self.AliveCheckScheduler = routines.scheduleFunction( self._AliveCheckScheduler, { self }, timer.getTime() + 1, 5 )
  self.AliveCheckScheduler = SCHEDULER:New( self, self._AliveCheckScheduler, { "Client Alive " .. ClientName }, 1, 5 )

  self:F( self )
  return self
end


--- Transport defines that the Client is a Transport. Transports show cargo.
-- @param #CLIENT self
-- @return #CLIENT
function CLIENT:Transport()
  self:F()

  self.ClientTransport = true
  return self
end

--- AddBriefing 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:F( ClientBriefing )
  self.ClientBriefing = ClientBriefing
  self.ClientBriefingShown = false
  
  return self
end

--- Show the briefing of a CLIENT.
-- @param #CLIENT self
-- @return #CLIENT self
function CLIENT:ShowBriefing()
  self:F( { self.ClientName, self.ClientBriefingShown } )

  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.
-- @param #CLIENT self
-- @param #function CallBackFunction Create a function that will be called when a player joins the slot.
-- @return #CLIENT
function CLIENT:Alive( CallBackFunction, ... )
  self:F()
  
  self.ClientCallBack = CallBackFunction
  self.ClientParameters = arg

  return self
end

--- @param #CLIENT self
function CLIENT:_AliveCheckScheduler( SchedulerName )
  self:F3( { SchedulerName, self.ClientName, self.ClientAlive2, self.ClientBriefingShown, self.ClientCallBack } )

  if self:IsAlive() then 
    if self.ClientAlive2 == false then
      self:ShowBriefing()
      if self.ClientCallBack then
        self:T("Calling Callback function")
        self.ClientCallBack( self, unpack( self.ClientParameters ) )
      end
      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
	
	self.ClientGroupID = nil
	self.ClientGroupUnit = nil
	
	return nil
end 


-- TODO: Check DCS#Group.ID
--- Get the group ID of the client.
-- @param #CLIENT self
-- @return DCS#Group.ID
function CLIENT:GetClientGroupID()

  local ClientGroup = self:GetDCSGroup()

  --self:F( self.ClientGroupID ) -- Determined in GetDCSGroup()
	return self.ClientGroupID
end


--- Get the name of the group of the client.
-- @param #CLIENT self
-- @return #string
function CLIENT:GetClientGroupName()

  local ClientGroup = self:GetDCSGroup()

  self:T( self.ClientGroupName ) -- Determined in GetDCSGroup()
	return self.ClientGroupName
end

--- Returns the UNIT of the CLIENT.
-- @param #CLIENT self
-- @return Wrapper.Unit#UNIT
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 )
		self:T2( ClientUnit )
		return ClientUnit
	end
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
--- **Wrapper** -- STATIC wraps the DCS StaticObject class.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @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 _@{DATABASE} object.
-- 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 _DATABASE object using a DCS Static name.
--  
-- IMPORTANT: ONE SHOULD NEVER SANATIZE these STATIC OBJECT REFERENCES! (make the STATIC object references nil).
-- 
-- @field #STATIC
STATIC = {
	ClassName = "STATIC",
}


function STATIC:Register( StaticName )
  local self = BASE:Inherit( self, POSITIONABLE:New( StaticName ) )
  self.StaticName = StaticName
  return self
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
function STATIC:FindByName( StaticName, RaiseError )
  local StaticFound = _DATABASE:FindStatic( StaticName )

  self.StaticName = StaticName
  
  if StaticFound then
    StaticFound:F3( { StaticName } )
  	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()
  end

  return nil
end



function STATIC:GetDCSObject()
  local DCSStatic = StaticObject.getByName( self.StaticName )
  
  if DCSStatic then
    return DCSStatic
  end
    
  return nil
end

--- Returns a list of one @{Static}.
-- @param #STATIC self
-- @return #list<Wrapper.Static#STATIC> A list of one @{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




function STATIC:GetThreatLevel()

  return 1, "Static"
end

--- Respawn the @{Wrapper.Unit} using a (tweaked) template of the parent Group.
-- @param #STATIC self
-- @param Core.Point#COORDINATE Coordinate The coordinate where to spawn the new Static.
-- @param #number Heading The heading of the unit respawn.
function STATIC:SpawnAt( Coordinate, Heading )

  local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName )
  
  SpawnStatic:SpawnFromPointVec2( Coordinate, Heading, self.StaticName )
end


--- Respawn the @{Wrapper.Unit} 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 The country ID used for spawning the new static.
function STATIC:ReSpawn(countryid)

  local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName, countryid )
  
  SpawnStatic:ReSpawn()
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 The heading of the unit respawn.
function STATIC:ReSpawnAt( Coordinate, Heading )

  local SpawnStatic = SPAWNSTATIC:NewFromStatic( self.StaticName )
  
  SpawnStatic:ReSpawnAt( Coordinate, Heading )
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
-- @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 _@{DATABASE} object.
-- 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 _DATABASE object using a DCS Airbase object.
--  * @{#AIRBASE.FindByName}(): Find a AIRBASE instance from the _DATABASE object using a DCS Airbase name.
--  
-- IMPORTANT: ONE SHOULD NEVER SANATIZE 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}().
-- 
-- @field #AIRBASE AIRBASE
AIRBASE = {
  ClassName="AIRBASE",
  CategoryName = { 
    [Airbase.Category.AIRDROME]   = "Airdrome",
    [Airbase.Category.HELIPAD]    = "Helipad",
    [Airbase.Category.SHIP]       = "Ship",
    },
  }

--- Enumeration to identify the airbases in the Caucasus region.
-- 
-- These are all airbases of Caucasus:
-- 
--   * AIRBASE.Caucasus.Gelendzhik
--   * AIRBASE.Caucasus.Krasnodar_Pashkovsky
--   * AIRBASE.Caucasus.Sukhumi_Babushara
--   * AIRBASE.Caucasus.Gudauta
--   * AIRBASE.Caucasus.Batumi
--   * AIRBASE.Caucasus.Senaki_Kolkhi
--   * AIRBASE.Caucasus.Kobuleti
--   * AIRBASE.Caucasus.Kutaisi
--   * AIRBASE.Caucasus.Tbilisi_Lochini
--   * AIRBASE.Caucasus.Soganlug
--   * AIRBASE.Caucasus.Vaziani
--   * AIRBASE.Caucasus.Anapa_Vityazevo
--   * AIRBASE.Caucasus.Krasnodar_Center
--   * AIRBASE.Caucasus.Novorossiysk
--   * AIRBASE.Caucasus.Krymsk
--   * AIRBASE.Caucasus.Maykop_Khanskaya
--   * AIRBASE.Caucasus.Sochi_Adler
--   * AIRBASE.Caucasus.Mineralnye_Vody
--   * AIRBASE.Caucasus.Nalchik
--   * AIRBASE.Caucasus.Mozdok
--   * AIRBASE.Caucasus.Beslan
--   
-- @field Caucasus
AIRBASE.Caucasus = {
  ["Gelendzhik"] = "Gelendzhik",
  ["Krasnodar_Pashkovsky"] = "Krasnodar-Pashkovsky",
  ["Sukhumi_Babushara"] = "Sukhumi-Babushara",
  ["Gudauta"] = "Gudauta",
  ["Batumi"] = "Batumi",
  ["Senaki_Kolkhi"] = "Senaki-Kolkhi",
  ["Kobuleti"] = "Kobuleti",
  ["Kutaisi"] = "Kutaisi",
  ["Tbilisi_Lochini"] = "Tbilisi-Lochini",
  ["Soganlug"] = "Soganlug",
  ["Vaziani"] = "Vaziani",
  ["Anapa_Vityazevo"] = "Anapa-Vityazevo",
  ["Krasnodar_Center"] = "Krasnodar-Center",
  ["Novorossiysk"] = "Novorossiysk",
  ["Krymsk"] = "Krymsk",
  ["Maykop_Khanskaya"] = "Maykop-Khanskaya",
  ["Sochi_Adler"] = "Sochi-Adler",
  ["Mineralnye_Vody"] = "Mineralnye Vody",
  ["Nalchik"] = "Nalchik",
  ["Mozdok"] = "Mozdok",
  ["Beslan"] = "Beslan",
  }

--- These are all airbases of Nevada:
-- 
--   * AIRBASE.Nevada.Creech_AFB
--   * AIRBASE.Nevada.Groom_Lake_AFB
--   * AIRBASE.Nevada.McCarran_International_Airport
--   * AIRBASE.Nevada.Nellis_AFB
--   * AIRBASE.Nevada.Beatty_Airport
--   * AIRBASE.Nevada.Boulder_City_Airport
--   * AIRBASE.Nevada.Echo_Bay
--   * AIRBASE.Nevada.Henderson_Executive_Airport
--   * AIRBASE.Nevada.Jean_Airport
--   * AIRBASE.Nevada.Laughlin_Airport
--   * AIRBASE.Nevada.Lincoln_County
--   * AIRBASE.Nevada.Mellan_Airstrip
--   * AIRBASE.Nevada.Mesquite
--   * AIRBASE.Nevada.Mina_Airport_3Q0
--   * AIRBASE.Nevada.North_Las_Vegas
--   * AIRBASE.Nevada.Pahute_Mesa_Airstrip
--   * AIRBASE.Nevada.Tonopah_Airport
--   * AIRBASE.Nevada.Tonopah_Test_Range_Airfield
-- @field Nevada 
AIRBASE.Nevada = {
  ["Creech_AFB"] = "Creech AFB",
  ["Groom_Lake_AFB"] = "Groom Lake AFB",
  ["McCarran_International_Airport"] = "McCarran International Airport",
  ["Nellis_AFB"] = "Nellis AFB",
  ["Beatty_Airport"] = "Beatty Airport",
  ["Boulder_City_Airport"] = "Boulder City Airport",
  ["Echo_Bay"] = "Echo Bay",
  ["Henderson_Executive_Airport"] = "Henderson Executive Airport",
  ["Jean_Airport"] = "Jean Airport",
  ["Laughlin_Airport"] = "Laughlin Airport",
  ["Lincoln_County"] = "Lincoln County",
  ["Mellan_Airstrip"] = "Mellan Airstrip",
  ["Mesquite"] = "Mesquite",
  ["Mina_Airport_3Q0"] = "Mina Airport 3Q0",
  ["North_Las_Vegas"] = "North Las Vegas",
  ["Pahute_Mesa_Airstrip"] = "Pahute Mesa Airstrip",
  ["Tonopah_Airport"] = "Tonopah Airport",
  ["Tonopah_Test_Range_Airfield"] = "Tonopah Test Range Airfield",
  }

--- These are all airbases of Normandy:
-- 
--   * AIRBASE.Normandy.Saint_Pierre_du_Mont
--   * AIRBASE.Normandy.Lignerolles
--   * AIRBASE.Normandy.Cretteville
--   * AIRBASE.Normandy.Maupertus
--   * AIRBASE.Normandy.Brucheville
--   * AIRBASE.Normandy.Meautis
--   * AIRBASE.Normandy.Cricqueville_en_Bessin
--   * AIRBASE.Normandy.Lessay
--   * AIRBASE.Normandy.Sainte_Laurent_sur_Mer
--   * AIRBASE.Normandy.Biniville
--   * AIRBASE.Normandy.Cardonville
--   * AIRBASE.Normandy.Deux_Jumeaux
--   * AIRBASE.Normandy.Chippelle
--   * AIRBASE.Normandy.Beuzeville
--   * AIRBASE.Normandy.Azeville
--   * AIRBASE.Normandy.Picauville
--   * AIRBASE.Normandy.Le_Molay
--   * AIRBASE.Normandy.Longues_sur_Mer
--   * AIRBASE.Normandy.Carpiquet
--   * AIRBASE.Normandy.Bazenville
--   * AIRBASE.Normandy.Sainte_Croix_sur_Mer
--   * AIRBASE.Normandy.Beny_sur_Mer
--   * AIRBASE.Normandy.Rucqueville
--   * AIRBASE.Normandy.Sommervieu
--   * AIRBASE.Normandy.Lantheuil
--   * AIRBASE.Normandy.Evreux
--   * AIRBASE.Normandy.Chailey
--   * AIRBASE.Normandy.Needs_Oar_Point
--   * AIRBASE.Normandy.Funtington
--   * AIRBASE.Normandy.Tangmere
--   * AIRBASE.Normandy.Ford
-- @field Normandy
AIRBASE.Normandy = {
  ["Saint_Pierre_du_Mont"] = "Saint Pierre du Mont",
  ["Lignerolles"] = "Lignerolles",
  ["Cretteville"] = "Cretteville",
  ["Maupertus"] = "Maupertus",
  ["Brucheville"] = "Brucheville",
  ["Meautis"] = "Meautis",
  ["Cricqueville_en_Bessin"] = "Cricqueville-en-Bessin",
  ["Lessay"] = "Lessay",
  ["Sainte_Laurent_sur_Mer"] = "Sainte-Laurent-sur-Mer",
  ["Biniville"] = "Biniville",
  ["Cardonville"] = "Cardonville",
  ["Deux_Jumeaux"] = "Deux Jumeaux",
  ["Chippelle"] = "Chippelle",
  ["Beuzeville"] = "Beuzeville",
  ["Azeville"] = "Azeville",
  ["Picauville"] = "Picauville",
  ["Le_Molay"] = "Le Molay",
  ["Longues_sur_Mer"] = "Longues-sur-Mer",
  ["Carpiquet"] = "Carpiquet",
  ["Bazenville"] = "Bazenville",
  ["Sainte_Croix_sur_Mer"] = "Sainte-Croix-sur-Mer",
  ["Beny_sur_Mer"] = "Beny-sur-Mer",
  ["Rucqueville"] = "Rucqueville",
  ["Sommervieu"] = "Sommervieu",
  ["Lantheuil"] = "Lantheuil",
  ["Evreux"] = "Evreux",
  ["Chailey"] = "Chailey",
  ["Needs_Oar_Point"] = "Needs Oar Point",
  ["Funtington"] = "Funtington",
  ["Tangmere"] = "Tangmere",
  ["Ford"] = "Ford",
  }

--- These are all airbases of the Persion Gulf Map:
-- 
-- * AIRBASE.PersianGulf.Fujairah_Intl
-- * AIRBASE.PersianGulf.Qeshm_Island
-- * AIRBASE.PersianGulf.Sir_Abu_Nuayr
-- * AIRBASE.PersianGulf.Abu_Musa_Island_Airport
-- * AIRBASE.PersianGulf.Bandar_Abbas_Intl
-- * AIRBASE.PersianGulf.Bandar_Lengeh
-- * AIRBASE.PersianGulf.Tunb_Island_AFB
-- * AIRBASE.PersianGulf.Havadarya
-- * AIRBASE.PersianGulf.Lar_Airbase
-- * AIRBASE.PersianGulf.Sirri_Island
-- * AIRBASE.PersianGulf.Tunb_Kochak
-- * AIRBASE.PersianGulf.Al_Dhafra_AB
-- * AIRBASE.PersianGulf.Dubai_Intl
-- * AIRBASE.PersianGulf.Al_Maktoum_Intl
-- * AIRBASE.PersianGulf.Khasab
-- * AIRBASE.PersianGulf.Al_Minhad_AB
-- * AIRBASE.PersianGulf.Sharjah_Intl
-- * AIRBASE.PersianGulf.Shiraz_International_Airport
-- * AIRBASE.PersianGulf.Kerman_Airport
-- @field PersianGulf
AIRBASE.PersianGulf = {
  ["Fujairah_Intl"] = "Fujairah Intl",
  ["Qeshm_Island"] = "Qeshm Island",
  ["Sir_Abu_Nuayr"] = "Sir Abu Nuayr",
  ["Abu_Musa_Island_Airport"] = "Abu Musa Island Airport",
  ["Bandar_Abbas_Intl"] = "Bandar Abbas Intl",
  ["Bandar_Lengeh"] = "Bandar Lengeh",
  ["Tunb_Island_AFB"] = "Tunb Island AFB",
  ["Havadarya"] = "Havadarya",
  ["Lar_Airbase"] = "Lar Airbase",
  ["Sirri_Island"] = "Sirri Island",
  ["Tunb_Kochak"] = "Tunb Kochak",
  ["Al_Dhafra_AB"] = "Al Dhafra AB",
  ["Dubai_Intl"] = "Dubai Intl",
  ["Al_Maktoum_Intl"] = "Al Maktoum Intl",
  ["Khasab"] = "Khasab",
  ["Al_Minhad_AB"] = "Al Minhad AB",
  ["Sharjah_Intl"] = "Sharjah Intl",
  ["Shiraz_International_Airport"] = "Shiraz International Airport",
  ["Kerman_Airport"] = "Kerman Airport",
  }
  
--- 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.
 
--- 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.HelicopterUnsable = 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 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 HelicopterUnsable 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.
AIRBASE.TerminalType = {
  Runway=16,
  HelicopterOnly=40,
  Shelter=68,
  OpenMed=72,
  OpenBig=104,
  OpenMedOrBig=176,
  HelicopterUsable=216,
  FighterAircraft=244,
}

-- Registration.
  
--- Create a new AIRBASE from DCSAirbase.
-- @param #AIRBASE self
-- @param #string AirbaseName The name of the airbase.
-- @return Wrapper.Airbase#AIRBASE
function AIRBASE:Register( AirbaseName )

  local self = BASE:Inherit( self, POSITIONABLE:New( AirbaseName ) )
  self.AirbaseName = AirbaseName
  self.AirbaseZone = ZONE_RADIUS:New( AirbaseName, self:GetVec2(), 2500 )
  return self
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

--- Get the DCS object of an airbase
-- @param #AIRBASE self
-- @return DCS#Airbase DCS airbase object.
function AIRBASE:GetDCSObject()
  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 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.
-- @return #table Table containing all airbase objects of the current map.
function AIRBASE.GetAllAirbases(coalition)
  
  local airbases={}
  for _,airbase in pairs(_DATABASE.AIRBASES) do
    if (coalition~=nil and airbase:GetCoalition()==coalition) or coalition==nil then
      table.insert(airbases, airbase)
    end
  end
  
  return airbases
end


--- 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 pairs(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
-- @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 _free=_isfree(_spot)
      local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos)
      table.insert(spots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=_free, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW})
    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
      if (allowTOAC and allowTOAC==true) or _spot.TO_AC==false then
        local _coord=COORDINATE:NewFromVec3(_spot.vTerminalPos)
        table.insert(freespots, {Coordinate=_coord, TerminalID=_spot.Term_Index, TerminalType=_spot.Term_Type, TOAC=_spot.TO_AC, Free=true, TerminalID0=_spot.Term_Index_0, DistToRwy=_spot.fDistToRW})
      end
    end
  end
  
  return freespots
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 termial 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  
  
  -- Get the size of an object.
  local function _GetObjectSize(unit,mooseobject)
    if mooseobject then
      unit=unit:GetDCSObject()
    end
    if unit and unit:isExist() then
      local DCSdesc=unit:getDesc()
      if DCSdesc.box then
        local x=DCSdesc.box.max.x+math.abs(DCSdesc.box.min.x)
        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)
        return math.max(x,z), x , y, z
      end
    end
    return 0,0,0,0
  end
  
  -- Function calculating the overlap of two (square) objects.
  local function _overlap(object1, mooseobject1, object2, mooseobject2, dist)
    local l1=_GetObjectSize(object1, mooseobject1)
    local l2=_GetObjectSize(object2, mooseobject2)
    local safedist=(l1/2+l2/2)*1.1    
    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 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=group:GetUnit(1)
  local _aircraftsize, ax,ay,az=_GetObjectSize(aircraft, true)
  
  -- 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:E(string.format("%s: Looking for %d parking spot(s) for aircraft of size %.1f m (x=%.1f,y=%.1f,z=%.1f) at termial 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
    
    if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) 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:E(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
          -- Unis are now returned as MOOSE units not DCS units!
          --local _vec3=unit:getPoint()
          --local _coord=COORDINATE:NewFromVec3(_vec3)
          local _coord=unit:GetCoordinate()
          local _dist=_coord:Get2DDistance(_spot)      
          local _safe=_overlap(aircraft, true, unit, true,_dist)
          
          if markobstacles then
            local l,x,y,z=_GetObjectSize(unit)      
            _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 _vec3=static:getPoint()
          local _coord=COORDINATE:NewFromVec3(_vec3)
          local _dist=_coord:Get2DDistance(_spot)      
          local _safe=_overlap(aircraft, true, static, false,_dist)
          
          if markobstacles then
            local l,x,y,z=_GetObjectSize(static)
            _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 _vec3=scenery:getPoint()
          local _coord=COORDINATE:NewFromVec3(_vec3)
          local _dist=_coord:Get2DDistance(_spot)
          local _safe=_overlap(aircraft, true, scenery, false,_dist)
          
          if markobstacles then
            local l,x,y,z=_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, true, aircraft, true,_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:E(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
        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

--- 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:GetDesc().category~=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)
    
    -- Mark runway spawn points.
    --[[
    for _i,_coord in pairs(runwaypoints) do
      _coord:MarkToAll(string.format("runway %d",_i))
    end
    ]]
    
    -- 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

--- Helper function to check for the correct terminal type including "artificial" ones.
-- @param #number Term_Type Termial 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
  end
  
  return match
end--- **Wrapper** -- SCENERY models scenery within the DCS simulator.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @module Wrapper.Scenery
-- @image Wrapper_Scenery.JPG



--- @type SCENERY
-- @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",
}


function SCENERY:Register( SceneryName, SceneryObject )
  local self = BASE:Inherit( self, POSITIONABLE:New( SceneryName ) )
  self.SceneryName = SceneryName
  self.SceneryObject = SceneryObject
  return self
end

function SCENERY:GetDCSObject()
  return self.SceneryObject
end

function SCENERY:GetThreatLevel()

  return 0, "Scenery"
end
--- **Core** -- Management of CARGO logistics, that can be transported from and to transportation carriers.
--
-- ===
-- 
-- # 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 manualy 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 capatility to make an APC group handle cargo.
--   - @{AI.AI_Cargo_Helicopter} will create for you the capatility 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_Helicopters} derived classes will create for your dynamic cargo handlers controlled by AI helicpter 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 capabilties.
-- 
-- # 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 dissapear 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 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,RR=500,NR=25)`
-- 
-- This will create a CARGO_CRATE object:
-- 
--    * with the group name `Static #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 dissapear 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,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.
-- 
-- ===
-- 
-- ### 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#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) 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#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) 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#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) 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#POINT_VEC2 ToPointVec2 (optional) @{Core.Point#POINT_VEC2) 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_Plane#AI_CARGO_PLANE} 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:F( { 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
    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:F()
  
  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:F( { 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:F( { 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:F( { PointVec2 = PointVec2, NearRadius = NearRadius } )
  
    if self.CargoObject:IsAlive() then
      --local Distance = PointVec2:Get2DDistance( self.CargoObject:GetPointVec2() )
      --self:F( { CargoObjectName = self.CargoObject:GetName() } )
      --self:F( { CargoObjectVec2 = self.CargoObject:GetVec2() } )
      --self:F( { PointVec2 = PointVec2:GetVec2() } )
      local Distance = Coordinate:Get2DDistance( self.CargoObject:GetCoordinate() )
      --self:F( { Distance = Distance, NearRadius = NearRadius or "nil" }  )
      
      if Distance <= NearRadius then
        --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = true } )
        return true
      end
    end
    
    --self:F( { PointVec2 = PointVec2, NearRadius = NearRadius, IsNear = false } )
    return false
  end
  
  
  
  --- Check if Cargo is the given @{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:F( { Zone } )
  
    if self:IsLoaded() then
      return Zone:IsPointVec2InZone( self.CargoCarrier:GetPointVec2() )
    else
      --self:F( { 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#POINT_VEC2
  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 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 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 #string Type
  -- @param #string Name
  -- @param #number LoadRadius (optional)
  -- @param #number NearRadius (optional)
  -- @return #CARGO_REPRESENTABLE
  function CARGO_REPRESENTABLE:New( CargoObject, Type, Name, LoadRadius, NearRadius )
    local self = BASE:Inherit( self, CARGO:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_REPRESENTABLE
    self:F( { Type, Name, LoadRadius, NearRadius } )

    local Desc = CargoObject:GetDesc()
    self:I( { Desc = Desc } )
    local Weight = math.random( 80, 120 )
    if Desc then
      if Desc.typeName == "2B11 mortar" then
        Weight = 210
      else
        Weight = Desc.massEmpty
      end
    end

    self:SetWeight( Weight )      

--      local Box = CargoUnit:GetBoundingBox()
--      local VolumeUnit = ( Box.max.x - Box.min.x ) * ( Box.max.y - Box.min.y ) * ( Box.max.z - Box.min.z ) 
--      self:I( { VolumeUnit = VolumeUnit, WeightUnit = WeightUnit } )
    --self:SetVolume( VolumeUnit )

    
    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:F( { CargoName = self:GetName() } )
    --_EVENTDISPATCHER:CreateEventDeleteCargo( self )
  
    return self
  end
  
  --- Route a cargo unit to a PointVec2.
  -- @param #CARGO_REPRESENTABLE self
  -- @param Core.Point#POINT_VEC2 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:F({NearUnit=NearUnit})
      local NearUnitCoalition = NearUnit:GetCoalition()
      local CargoCoalition = self:GetCoalition()
      if NearUnitCoalition == CargoCoalition then
        local Attributes = NearUnit:GetDesc()
        self:F({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:F( { 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:F( { 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:F()

  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:F()

  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:F()

  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:F()

  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:F()

  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:F()

  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:F()
  
  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 @{Wrapper.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.
  -- 
  -- ===
  -- 
  -- @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 )
    local self = BASE:Inherit( self, CARGO_REPRESENTABLE:New( CargoUnit, Type, Name, LoadRadius, NearRadius ) ) -- #CARGO_UNIT
    self:I( { Type, Name, LoadRadius, NearRadius } )
  
    self:T( CargoUnit )
    self.CargoObject = CargoUnit
  
    self:T( self.ClassName )
  
    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#POINT_VEC2 ToPointVec2
  -- @param #number NearRadius (optional) Defaut 25 m.
  function CARGO_UNIT:onenterUnBoarding( From, Event, To, ToPointVec2, NearRadius )
    self:F( { 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
            self.CargoObject:ReSpawnAt( FromPointVec2, CargoDeployHeading )
            self:F( { "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#POINT_VEC2 ToPointVec2
  -- @param #number NearRadius (optional) Defaut 100 m.
  function CARGO_UNIT:onleaveUnBoarding( From, Event, To, ToPointVec2, NearRadius )
    self:F( { 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#POINT_VEC2 ToPointVec2
  -- @param #number NearRadius (optional) Defaut 100 m.
  function CARGO_UNIT:onafterUnBoarding( From, Event, To, ToPointVec2, NearRadius )
    self:F( { 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#POINT_VEC2
  function CARGO_UNIT:onenterUnLoaded( From, Event, To, ToPointVec2 )
    self:F( { 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:F( { 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:F({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:F( { From, Event, To, CargoCarrier:GetName(), NearRadius = NearRadius } )
    
    self:F( { 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:F({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:E("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:F( { From, Event, To, CargoCarrier } )
  
    self.CargoCarrier = CargoCarrier
    
    --self:F({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.
  -- 
  -- ===
  -- 
  -- @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:F( { 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:F( { 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:F( { 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:F()
    
    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:F( {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:F( {NearRadius = NearRadius } )
    
    return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius )
  end


  --- Respawn the CargoGroup.
  -- @param #CARGO_SLINGLOAD self
  function CARGO_SLINGLOAD:Respawn()

    --self:F( { "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:F( { "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
  
  --- 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.
  -- 
  -- ===
  -- 
  -- @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:F( { 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#POINT_VEC2
  function CARGO_CRATE:onenterUnLoaded( From, Event, To, ToPointVec2 )
    --self:F( { 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:F( { 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:F( { 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:F( { 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:F()
    
    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:F( {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:F( {NearRadius = NearRadius } )
    
    return self:IsNear( CargoCarrier:GetCoordinate(), NearRadius )
  end

  --- Respawn the CargoGroup.
  -- @param #CARGO_CRATE self
  function CARGO_CRATE:Respawn()

    self:F( { "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:F( { "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 @{Wrapper.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.
  -- @extends Cargo.Cargo#CARGO_REPORTABLE
  
  --- Defines a cargo that is represented by a @{Wrapper.Group} object within the simulator.
  -- 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.
  -- 
  -- @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 @{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 )
    local self = BASE:Inherit( self, CARGO_REPORTABLE:New( Type, Name, 0, LoadRadius, NearRadius ) ) -- #CARGO_GROUP
    self:F( { 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 ) )

    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:F( { "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:F("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:F( { 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:F( { "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:E(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:E( { "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:F( { 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:F( { "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:F( { 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:F( { 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:F("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#POINT_VEC2 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:F( {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#POINT_VEC2 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:F( { 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#POINT_VEC2
  function CARGO_GROUP:onafterUnLoad( From, Event, To, ToPointVec2, ... )
    --self:F( { 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=ToPointVec2:GetRandomPointVec2InRadius(20, 10)
          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 amount of cargo units in the 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:F( {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:F( {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:F( "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:F( { 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:F( { 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:F( { Coordinate } )
  
    local Cargo = self:GetFirstAlive() -- Cargo.Cargo#CARGO

    if Cargo then
      self:F( { 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 @{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:F( { 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, and create a CSV file logging the scoring events for use at team or squadron 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.
--   * 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/SCO%20-%20Scoring)
-- 
-- ===
-- 
-- 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}, @{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 @{Zone}s can be defined for which scores are also granted when objects in that @{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 amoung 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:
-- 
--   * **Wingthor (TAW)**: Testing & Advice.
--   * **Dutch-Baron (TAW)**: Testing & Advice.
--   * **[Whisper](http://forums.eagle.ru/member.php?u=3829): 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 @{Static}s.  
-- Use the method @{#SCORING.SetGroupGroup}() to specify a special additional score for a specific @{Wrapper.Group}s.  
-- 
--      local Scoring = SCORING:New( "Scoring File" )
--      Scoring:AddUnitScore( UNIT:FindByName( "Unit #001" ), 200 )
--      Scoring:AddStaticScore( STATIC:FindByName( "Static #1" ), 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 @{Zone} for additional scoring.  
-- Use the method @{#SCORING.RemoveZoneScore}() to remove a @{Zone} for additional scoring.  
-- There are interesting variations that can be achieved with this functionality. For example, if the @{Zone} is a @{Core.Zone#ZONE_UNIT}, 
-- then the zone is a moving zone, and anything destroyed within that @{Zone} will generate points.
-- The other implementation could be to designate a scenery target (a building) in the mission editor surrounded by a @{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 decomissioned 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 mutiplier * 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 potentialy harmful lua code that may use these functions.
--        --You can remove the code below and make availble 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 = {},
}

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.
-- @return #SCORING self
-- @usage
-- 
-- -- Define a new scoring object for the mission Gori Valley.
-- ScoringObject = SCORING:New( "Gori Valley" )
-- 
function SCORING:New( GameName )

  -- 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 )

  -- Default fratricide penalty level (maximum penalty that can be assigned to a player before he gets kicked).
  self:SetFratricide( self.ScaleDestroyPenalty * 3 )
  
  -- Default penalty when a player changes coalition.
  self:SetCoalitionChangePenalty( self.ScaleDestroyPenalty )
  
  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:OpenCSV( GameName )

  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 @{Static} for additional scoring when the @{Static} is destroyed.
-- Note that if there was already a @{Static} declared within the scoring with the same name, 
-- then the old @{Static}  will be replaced with the new @{Static}.
-- @param #SCORING self
-- @param Wrapper.Static#UNIT ScoreStatic The @{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 @{Static} for additional scoring when the @{Static} is destroyed.
-- @param #SCORING self
-- @param Wrapper.Static#UNIT ScoreStatic The @{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

--- Add a @{Zone} to define additional scoring when any object is destroyed in that zone.
-- Note that if a @{Zone} with the same name is already within the scoring added, the @{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 @{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 @{Zone} for additional scoring.
-- The scoring will search if any @{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 @{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

--- 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 mutiplier * 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


--- 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
      if self.Players[PlayerName].UnitCoalition ~= UnitCoalition then
        self.Players[PlayerName].Penalty = self.Players[PlayerName].Penalty + 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). 50 Penalty points added.",
          MESSAGE.Type.Information
        ):ToAll()
        self:ScoreCSV( PlayerName, "", "COALITION_PENALTY",  1, -50, 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: DCS bug concerning Units with skill level client don't get destroyed in multi player. This logic is deactivated until this bug gets fixed.
    --[[
    if self.Players[PlayerName].Penalty > self.Fratricide * 0.50 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 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
  
    MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll()
  
    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:F( { 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
  
    MESSAGE:NewType( self.DisplayMessagePrefix .. Text, MESSAGE.Type.Information ):ToAll()
  
    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
  
    MESSAGE:NewType( self.DisplayMessagePrefix .. Mission:GetText() .. " : " .. Text .. " Score: " .. Score, MESSAGE.Type.Information ):ToAll()
  
    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
  
    MESSAGE:NewType( string.format( "%s%s: %s! Player %s receives %d score!", self.DisplayMessagePrefix, Mission:GetText(), Text, PlayerName, Score ), MESSAGE.Type.Information ):ToAll()

    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

      MESSAGE:NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' has " .. Text .. " in " .. Mission:GetText() .. ". " ..
        Score .. " mission score!",
        MESSAGE.Type.Information ):ToAll()

      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
    if Event.IniObjectCategory == 1 then
      local PlayerName = Event.IniUnit:GetPlayerName()
      if PlayerName then
        self:_AddPlayerFromUnit( Event.IniUnit )
        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

  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

    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
        PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel()

        -- 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
              Player.Penalty = Player.Penalty + 10
              PlayerHit.Penalty = PlayerHit.Penalty + 10
              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: -" .. PlayerHit.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: -" .. PlayerHit.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 + 1
              PlayerHit.Score = PlayerHit.Score + 1
              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() )
              else
                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() )
              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 scenery object.",
                    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
        PlayerHit.ThreatLevel, PlayerHit.ThreatType = PlayerHit.UNIT:GetThreatLevel()

        -- 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
              Player.Penalty = Player.Penalty + 10
              PlayerHit.Penalty = PlayerHit.Penalty + 10
              PlayerHit.PenaltyHit = PlayerHit.PenaltyHit + 1
      
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit friendly target " .. 
                      TargetUnitCategory .. " ( " .. TargetType .. " ) " .. 
                      "Penalty: -" .. PlayerHit.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 + 1
              PlayerHit.Score = PlayerHit.Score + 1
              PlayerHit.ScoreHit = PlayerHit.ScoreHit + 1
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. Event.WeaponPlayerName .. "' hit enemy target " .. 
                      TargetUnitCategory .. " ( " .. TargetType .. " ) " .. 
                      "Score: +" .. PlayerHit.Score .. " = " .. 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 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
            
            if Player.HitPlayers[TargetPlayerName] then -- A player destroyed another player
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly player '" .. TargetPlayerName .. "' " .. 
                      TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. 
                      "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty,
                      MESSAGE.Type.Information 
                    )
                :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            else
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed friendly target " .. 
                      TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. 
                      "Penalty: -" .. TargetDestroy.Penalty .. " = " .. Player.Score - Player.Penalty,
                      MESSAGE.Type.Information 
                    )
                :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            end

            Destroyed = true
            self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_PENALTY", 1, ThreatPenalty, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
          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
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy player '" .. TargetPlayerName .. "' " .. 
                      TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. 
                      "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty,
                      MESSAGE.Type.Information 
                    )
                :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            else
              MESSAGE
                :NewType( self.DisplayMessagePrefix .. "Player '" .. PlayerName .. "' destroyed enemy " .. 
                      TargetUnitCategory .. " ( " .. ThreatTypeTarget .. " ) " .. 
                      "Score: +" .. TargetDestroy.Score .. " = " .. Player.Score - Player.Penalty,
                      MESSAGE.Type.Information 
                    )
                :ToAllIf( self:IfMessagesDestroy() and self:IfMessagesToAll() )
                :ToCoalitionIf( InitCoalition, self:IfMessagesDestroy() and self:IfMessagesToCoalition() )
            end
            Destroyed = true
            self:ScoreCSV( PlayerName, TargetPlayerName, "DESTROY_SCORE", 1, ThreatScore, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, TargetUnitCoalition, TargetUnitCategory, TargetUnitType )
            
            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() )
              Destroyed = true
              self:ScoreCSV( PlayerName, "", "DESTROY_SCORE", 1, Score, InitUnitName, InitUnitCoalition, InitUnitCategory, InitUnitType, TargetUnitName, "", "Scenery", TargetUnitType )
            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 + ScoreGoals + 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 + ScoreGoals + 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 + ScoreGoals + 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
    nHours = string.format("%02.f", math.floor(nSeconds/3600));
    nMins = string.format("%02.f", math.floor(nSeconds/60 - (nHours*60)));
    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 then
    if ScoringCSV then
      self.ScoringCSV = ScoringCSV
      local fdir = lfs.writedir() .. [[Logs\]] .. 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","PlayerUnitCoaltion","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 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


function SCORING:CloseCSV()
  if lfs and io and os then
    self.CSVFile:close()
  end
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/CLA%20-%20CleanUp%20Airbase)
-- 
-- ===
-- 
-- 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.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.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.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: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 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 Moveing 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 = routines.scheduleFunction( self._Scheduler, { self }, timer.getTime() + 1, 120 )
  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 new @{DATABASE} class 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 new @{DATABASE} class 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 execute evasive and 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.
-- 
-- ===
-- 
-- ## Missions:
-- 
-- [SEV - SEAD Evasion](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/SEV%20-%20SEAD%20Evasion)
-- 
-- ===
-- 
-- ### Authors: **FlightControl**
-- 
-- ===
-- 
-- @module Functional.Sead
-- @image SEAD.JPG

--- @type SEAD
-- @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.
-- 
-- # 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 = 50, DelayOff = { 10, 25 }, DelayOn = { 10, 30 } } ,
		Good      = { Evade = 30, DelayOff = { 8, 20 }, DelayOn = { 20, 40 } } ,
		High      = { Evade = 15, DelayOff = { 5, 17 }, DelayOn = { 30, 50 } } ,
		Excellent = { Evade = 10, DelayOff = { 3, 10 }, DelayOn = { 30, 60 } } 
	}, 
	SEADGroupPrefixes = {} 
}

--- 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 table{string,...}|string SEADGroupPrefixes which is a table of Prefixes of the SA Groups in the DCSRTE on which evasive actions need to be taken.
-- @return SEAD
-- @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 )
	local self = BASE:Inherit( self, BASE:New() )
	self:F( SEADGroupPrefixes )	
	if type( SEADGroupPrefixes ) == 'table' then
		for SEADGroupPrefixID, SEADGroupPrefix in pairs( SEADGroupPrefixes ) do
			self.SEADGroupPrefixes[SEADGroupPrefix] = SEADGroupPrefix
		end
	else
		self.SEADGroupNames[SEADGroupPrefixes] = SEADGroupPrefixes
	end
	
	self:HandleEvent( EVENTS.Shot )
	
	return self
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.
-- @see SEAD
-- @param #SEAD
-- @param Core.Event#EVENTDATA EventData
function SEAD:OnEventShot( EventData )
	self:F( { EventData } )

	local SEADUnit = EventData.IniDCSUnit
	local SEADUnitName = EventData.IniDCSUnitName
	local SEADWeapon = EventData.Weapon -- Identify the weapon fired						
	local SEADWeaponName = EventData.WeaponName	-- return weapon type
	-- Start of the 2nd loop
	self:T( "Missile Launched = " .. SEADWeaponName )
	if SEADWeaponName == "KH-58" or SEADWeaponName == "KH-25MPU" or SEADWeaponName == "AGM-88" or SEADWeaponName == "KH-31A" or SEADWeaponName == "KH-31P" then -- Check if the missile is a SEAD
		local _evade = math.random (1,100) -- random number for chance of evading action
		local _targetMim = EventData.Weapon:getTarget() -- Identify target
		local _targetMimname = Unit.getName(_targetMim)
		local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
		local _targetMimgroupName = _targetMimgroup:getName()
		local _targetMimcont= _targetMimgroup:getController()
		local _targetskill =  _DATABASE.Templates.Units[_targetMimname].Template.skill
		self:T( self.SEADGroupPrefixes )
		self:T( _targetMimgroupName )
		local SEADGroupFound = false
		for SEADGroupPrefixID, SEADGroupPrefix in pairs( self.SEADGroupPrefixes ) do
			if string.find( _targetMimgroupName, SEADGroupPrefix, 1, true ) then
				SEADGroupFound = true
				self:T( 'Group Found' )
				break
			end
		end		
		if SEADGroupFound == true then
			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
				if (_evade > self.TargetSkill[_targetskill].Evade) then
					self:T( string.format("Evading, target skill  " ..string.format(_targetskill)) )
					local _targetMim = Weapon.getTarget(SEADWeapon)
					local _targetMimname = Unit.getName(_targetMim)
					local _targetMimgroup = Unit.getGroup(Weapon.getTarget(SEADWeapon))
					local _targetMimcont= _targetMimgroup:getController()
					routines.groupRandomDistSelf(_targetMimgroup,300,'Diamond',250,20) -- move randomly
					local SuppressedGroups1 = {} -- unit suppressed radar off for a random time
					local function SuppressionEnd1(id)
						id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
						SuppressedGroups1[id.groupName] = nil
					end
					local id = {
					groupName = _targetMimgroup,
					ctrl = _targetMimcont
					}
					local delay1 = math.random(self.TargetSkill[_targetskill].DelayOff[1], self.TargetSkill[_targetskill].DelayOff[2])
					if SuppressedGroups1[id.groupName] == nil then
						SuppressedGroups1[id.groupName] = {
							SuppressionEndTime1 = timer.getTime() + delay1,
							SuppressionEndN1 = SuppressionEndCounter1	--Store instance of SuppressionEnd() scheduled function
						}	
						Controller.setOption(_targetMimcont, AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.GREEN)
						timer.scheduleFunction(SuppressionEnd1, id, SuppressedGroups1[id.groupName].SuppressionEndTime1)	--Schedule the SuppressionEnd() function
						--trigger.action.outText( string.format("Radar Off " ..string.format(delay1)), 20)
					end
					
					local SuppressedGroups = {}
					local function SuppressionEnd(id)
						id.ctrl:setOption(AI.Option.Ground.id.ALARM_STATE,AI.Option.Ground.val.ALARM_STATE.RED)
						SuppressedGroups[id.groupName] = nil
					end
					local id = {
						groupName = _targetMimgroup,
						ctrl = _targetMimcont
					}
					local delay = math.random(self.TargetSkill[_targetskill].DelayOn[1], self.TargetSkill[_targetskill].DelayOn[2])
					if SuppressedGroups[id.groupName] == nil then
						SuppressedGroups[id.groupName] = {
							SuppressionEndTime = timer.getTime() + delay,
							SuppressionEndN = SuppressionEndCounter	--Store instance of SuppressionEnd() scheduled function
						}
						timer.scheduleFunction(SuppressionEnd, id, SuppressedGroups[id.groupName].SuppressionEndTime)	--Schedule the SuppressionEnd() function
						--trigger.action.outText( string.format("Radar On " ..string.format(delay)), 20)
					end
				end
			end
		end
	end
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.
-- 
-- ===
-- 
-- ## Missions:
-- 
-- [ESC - Escorting](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ESC%20-%20Escorting)
-- 
-- ===
-- 
-- 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 Function.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 = 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
    routines.removeFunction( self.ReportTargetsScheduler )
    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:GetDetectedSet( 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:GetDetectedSet( 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:GetDetectedSet( 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:GetDetectedSet( 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

    if true 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
    else
--      local EscortGroupName = self.EscortGroup:GetName()
--      local EscortTargets = self.EscortGroup:GetDetectedTargets()
--  
--      local ClientEscortTargets = self.EscortClient._EscortGroups[EscortGroupName].Targets
--  
--      local EscortTargetMessages = ""
--      for EscortTargetID, EscortTarget in pairs( EscortTargets ) do
--        local EscortObject = EscortTarget.object
--        self:T( EscortObject )
--        if EscortObject and EscortObject:isExist() and EscortObject.id_ < 50000000 then
--  
--          local EscortTargetUnit = UNIT:Find( EscortObject )
--          local EscortTargetUnitName = EscortTargetUnit:GetName()
--  
--  
--  
--          --          local EscortTargetIsDetected,
--          --                EscortTargetIsVisible,
--          --                EscortTargetLastTime,
--          --                EscortTargetKnowType,
--          --                EscortTargetKnowDistance,
--          --                EscortTargetLastPos,
--          --                EscortTargetLastVelocity
--          --                = self.EscortGroup:IsTargetDetected( EscortObject )
--          --
--          --          self:T( { EscortTargetIsDetected,
--          --                EscortTargetIsVisible,
--          --                EscortTargetLastTime,
--          --                EscortTargetKnowType,
--          --                EscortTargetKnowDistance,
--          --                EscortTargetLastPos,
--          --                EscortTargetLastVelocity } )
--  
--  
--          local EscortTargetUnitVec3 = EscortTargetUnit:GetVec3()
--          local EscortVec3 = self.EscortGroup:GetVec3()
--          local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 +
--            ( EscortTargetUnitVec3.y - EscortVec3.y )^2 +
--            ( EscortTargetUnitVec3.z - EscortVec3.z )^2
--            ) ^ 0.5 / 1000
--  
--          self:T( { self.EscortGroup:GetName(), EscortTargetUnit:GetName(), Distance, EscortTarget } )
--  
--          if Distance <= 15 then
--  
--            if not ClientEscortTargets[EscortTargetUnitName] then
--              ClientEscortTargets[EscortTargetUnitName] = {}
--            end
--            ClientEscortTargets[EscortTargetUnitName].AttackUnit = EscortTargetUnit
--            ClientEscortTargets[EscortTargetUnitName].visible = EscortTarget.visible
--            ClientEscortTargets[EscortTargetUnitName].type = EscortTarget.type
--            ClientEscortTargets[EscortTargetUnitName].distance = EscortTarget.distance
--          else
--            if ClientEscortTargets[EscortTargetUnitName] then
--              ClientEscortTargets[EscortTargetUnitName] = nil
--            end
--          end
--        end
--      end
--  
--      self:T( { "Sorting Targets Table:", ClientEscortTargets } )
--      table.sort( ClientEscortTargets, function( a, b ) return a.Distance < b.Distance end )
--      self:T( { "Sorted Targets Table:", ClientEscortTargets } )
--  
--      -- Remove the sub menus of the Attack menu of the Escort for the EscortGroup.
--      self.EscortMenuAttackNearbyTargets:RemoveSubMenus()
--  
--      if self.EscortMenuTargetAssistance then
--        self.EscortMenuTargetAssistance:RemoveSubMenus()
--      end
--  
--      --for MenuIndex = 1, #self.EscortMenuAttackTargets do
--      --  self:T( { "Remove Menu:", self.EscortMenuAttackTargets[MenuIndex] } )
--      --  self.EscortMenuAttackTargets[MenuIndex] = self.EscortMenuAttackTargets[MenuIndex]:Remove()
--      --end
--  
--  
--      if ClientEscortTargets then
--        for ClientEscortTargetUnitName, ClientEscortTargetData in pairs( ClientEscortTargets ) do
--  
--          for ClientEscortGroupName, EscortGroupData in pairs( self.EscortClient._EscortGroups ) do
--  
--            if ClientEscortTargetData and ClientEscortTargetData.AttackUnit:IsAlive() then
--  
--              local EscortTargetMessage = ""
--              local EscortTargetCategoryName = ClientEscortTargetData.AttackUnit:GetCategoryName()
--              local EscortTargetCategoryType = ClientEscortTargetData.AttackUnit:GetTypeName()
--              if ClientEscortTargetData.type then
--                EscortTargetMessage = EscortTargetMessage .. EscortTargetCategoryName .. " (" .. EscortTargetCategoryType .. ") at "
--              else
--                EscortTargetMessage = EscortTargetMessage .. "Unknown target at "
--              end
--  
--              local EscortTargetUnitVec3 = ClientEscortTargetData.AttackUnit:GetVec3()
--              local EscortVec3 = self.EscortGroup:GetVec3()
--              local Distance = ( ( EscortTargetUnitVec3.x - EscortVec3.x )^2 +
--                ( EscortTargetUnitVec3.y - EscortVec3.y )^2 +
--                ( EscortTargetUnitVec3.z - EscortVec3.z )^2
--                ) ^ 0.5 / 1000
--  
--              self:T( { self.EscortGroup:GetName(), ClientEscortTargetData.AttackUnit:GetName(), Distance, ClientEscortTargetData.AttackUnit } )
--              if ClientEscortTargetData.visible == false then
--                EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " estimated km"
--              else
--                EscortTargetMessage = EscortTargetMessage .. string.format( "%.2f", Distance ) .. " km"
--              end
--  
--              if ClientEscortTargetData.visible then
--                EscortTargetMessage = EscortTargetMessage .. ", visual"
--              end
--  
--              if ClientEscortGroupName == EscortGroupName then
--  
--                MENU_GROUP_COMMAND:New( self.EscortClient,
--                  EscortTargetMessage,
--                  self.EscortMenuAttackNearbyTargets,
--                  ESCORT._AttackTarget,
--                  { ParamSelf = self,
--                    ParamUnit = ClientEscortTargetData.AttackUnit
--                  }
--                )
--                EscortTargetMessages = EscortTargetMessages .. "\n - " .. EscortTargetMessage
--              else
--                if self.EscortMenuTargetAssistance then
--                  local MenuTargetAssistance = MENU_GROUP:New( self.EscortClient, EscortGroupData.EscortName, self.EscortMenuTargetAssistance )
--                  MENU_GROUP_COMMAND:New( self.EscortClient,
--                    EscortTargetMessage,
--                    MenuTargetAssistance,
--                    ESCORT._AssistTarget,
--                    self,
--                    EscortGroupData.EscortGroup,
--                    ClientEscortTargetData.AttackUnit
--                  )
--                end
--              end
--            else
--              ClientEscortTargetData = nil
--            end
--          end
--        end
--  
--        if EscortTargetMessages ~= "" and self.ReportTargets == true then
--          self.EscortGroup:MessageToClient( "Detected targets within 15 km range:" .. EscortTargetMessages:gsub("\n$",""), 20, self.EscortClient )
--        else
--          self.EscortGroup:MessageToClient( "No targets detected!", 20, self.EscortClient )
--        end
--      end
--  
--      if self.EscortMenuResumeMission then
--        self.EscortMenuResumeMission:RemoveSubMenus()
--  
--        --    if self.EscortMenuResumeWayPoints then
--        --      for MenuIndex = 1, #self.EscortMenuResumeWayPoints do
--        --        self:T( { "Remove Menu:", self.EscortMenuResumeWayPoints[MenuIndex] } )
--        --        self.EscortMenuResumeWayPoints[MenuIndex] = self.EscortMenuResumeWayPoints[MenuIndex]:Remove()
--        --      end
--        --    end
--  
--        local TaskPoints = self:RegisterRoute()
--        for WayPointID, WayPoint in pairs( TaskPoints ) do
--          local EscortVec3 = self.EscortGroup:GetVec3()
--          local Distance = ( ( WayPoint.x - EscortVec3.x )^2 +
--            ( WayPoint.y - EscortVec3.z )^2
--            ) ^ 0.5 / 1000
--          MENU_GROUP_COMMAND:New( self.EscortClient, "Waypoint " .. WayPointID .. " at " .. string.format( "%.2f", Distance ).. "km", self.EscortMenuResumeMission, ESCORT._ResumeMission, { ParamSelf = self, ParamWayPoint = WayPointID } )
--        end
--      end
--  
--      return true
    end
  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/MIT%20-%20Missile%20Trainer)
-- 
-- ===
-- 
-- 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 targetted 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) targetted 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.
--   
-- ===
-- 
-- ### 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 targetted 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.
-- 
-- @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 targetted 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 )
    --DirectionRadians = DirectionRadians + routines.getNorthCorrection( PositionTarget )
    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:
-- 
-- [ABP - Airbase Police](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/ABP%20-%20Airbase%20Police)
--
-- ===
--
-- ### Contributions: Dutch Baron - Concept & Testing
-- ### Author: FlightControl - Framework Design &  Programming
--
-- ===
-- 
-- @module Functional.ATC_Ground
-- @image Air_Traffic_Control_Ground_Operations.JPG

--- @type ATC_GROUND
-- @field Core.Set#SET_CLIENT SetClient
-- @extends Core.Base#BASE

--- Base class for ATC\_GROUND implementations.
-- @field #ATC_GROUND
ATC_GROUND = {
  ClassName = "ATC_GROUND",
  SetClient = nil,
  Airbases = nil,
  AirbaseNames = 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.
}

--- @type ATC_GROUND.AirbaseNames
-- @list <#string>


--- 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:E( { 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
    Airbase.ZoneBoundary = _DATABASE:FindAirbase( AirbaseID ):GetZone()
    Airbase.ZoneRunways = {}
    for PointsRunwayID, PointsRunway in pairs( Airbase.PointsRunways ) do
      Airbase.ZoneRunways[PointsRunwayID] = ZONE_POLYGON_BASE:New( "Runway " .. PointsRunwayID, PointsRunway )
    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

--    -- Template
--    local TemplateBoundary = GROUP:FindByName( "Template Boundary" )
--    self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
--  
--    local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" )
--    self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()

  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()
--   
--   -- 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()
--   
--   -- 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:E( 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:E( Taxi )
                if Taxi == false then
                  local Velocity = VELOCITY:New( AirbaseMeta.KickSpeed or self.KickSpeed )
                  Client:Message( "Welcome at " .. 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() .. 
                                 " is 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() .. " is 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() .. " is 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_CAUCASUS
-- @extends #ATC_GROUND

--- # ATC\_GROUND\_CAUCASUS, extends @{#ATC_GROUND}
-- 
-- 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",
  Airbases = {
    [AIRBASE.Caucasus.Anapa_Vityazevo] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=242140.57142858,["x"]=-6478.8571428583,},
          [2]={["y"]=242188.57142858,["x"]=-6522.0000000011,},
          [3]={["y"]=244124.2857143,["x"]=-4344.0000000011,},
          [4]={["y"]=244068.2857143,["x"]=-4296.5714285726,},
          [5]={["y"]=242140.57142858,["x"]=-6480.0000000011,}
        },
      },
    },
    [AIRBASE.Caucasus.Batumi] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=616442.28571429,["x"]=-355090.28571429,},
          [2]={["y"]=618450.57142857,["x"]=-356522,},
          [3]={["y"]=618407.71428571,["x"]=-356584.85714286,},
          [4]={["y"]=618361.99999999,["x"]=-356554.85714286,},
          [5]={["y"]=618324.85714285,["x"]=-356599.14285715,},
          [6]={["y"]=618250.57142856,["x"]=-356543.42857143,},
          [7]={["y"]=618257.7142857,["x"]=-356496.28571429,},
          [8]={["y"]=618237.7142857,["x"]=-356459.14285715,},
          [9]={["y"]=616555.71428571,["x"]=-355258.85714286,},
          [10]={["y"]=616486.28571428,["x"]=-355280.57142858,},
          [11]={["y"]=616410.57142856,["x"]=-355227.71428572,},
          [12]={["y"]=616441.99999999,["x"]=-355179.14285715,},
          [13]={["y"]=616401.99999999,["x"]=-355147.71428572,},
          [14]={["y"]=616441.42857142,["x"]=-355092.57142858,},
        },
      },
    },
    [AIRBASE.Caucasus.Beslan] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=842104.57142857,["x"]=-148460.57142857,},
          [2]={["y"]=845225.71428572,["x"]=-148656,},
          [3]={["y"]=845220.57142858,["x"]=-148750,},
          [4]={["y"]=842098.85714286,["x"]=-148556.28571429,},
          [5]={["y"]=842104,["x"]=-148460.28571429,},
        },
      },
    },
    [AIRBASE.Caucasus.Gelendzhik] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=297834.00000001,["x"]=-51107.428571429,},
          [2]={["y"]=297786.57142858,["x"]=-51068.857142858,},
          [3]={["y"]=298946.57142858,["x"]=-49686.000000001,},
          [4]={["y"]=298993.14285715,["x"]=-49725.714285715,},
          [5]={["y"]=297835.14285715,["x"]=-51107.714285715,},
        },
      },
    },
    [AIRBASE.Caucasus.Gudauta] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=517096.57142857,["x"]=-197804.57142857,},
          [2]={["y"]=515880.85714285,["x"]=-195590.28571429,},
          [3]={["y"]=515812.28571428,["x"]=-195628.85714286,},
          [4]={["y"]=517036.57142857,["x"]=-197834.57142857,},
          [5]={["y"]=517097.99999999,["x"]=-197807.42857143,},
        },
      },
    },
    [AIRBASE.Caucasus.Kobuleti] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=634509.71428571,["x"]=-318339.42857144,},
          [2]={["y"]=636767.42857143,["x"]=-317516.57142858,},
          [3]={["y"]=636790,["x"]=-317575.71428572,},
          [4]={["y"]=634531.42857143,["x"]=-318398.00000001,},
          [5]={["y"]=634510.28571429,["x"]=-318339.71428572,},
        },
      },
    },
    [AIRBASE.Caucasus.Krasnodar_Center] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=369205.42857144,["x"]=11789.142857142,},
          [2]={["y"]=369209.71428572,["x"]=11714.857142856,},
          [3]={["y"]=366699.71428572,["x"]=11581.714285713,},
          [4]={["y"]=366698.28571429,["x"]=11659.142857142,},
          [5]={["y"]=369208.85714286,["x"]=11788.57142857,},
        },
      },
    },
    [AIRBASE.Caucasus.Krasnodar_Pashkovsky] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=385891.14285715,["x"]=8416.5714285703,},
          [2]={["y"]=385842.28571429,["x"]=8467.9999999989,},
          [3]={["y"]=384180.85714286,["x"]=6917.1428571417,},
          [4]={["y"]=384228.57142858,["x"]=6867.7142857132,},
          [5]={["y"]=385891.14285715,["x"]=8416.5714285703,},
        },
        [2] = {
          [1]={["y"]=386714.85714286,["x"]=6674.857142856,},
          [2]={["y"]=386757.71428572,["x"]=6627.7142857132,},
          [3]={["y"]=389028.57142858,["x"]=8741.4285714275,},
          [4]={["y"]=388981.71428572,["x"]=8790.5714285703,},
          [5]={["y"]=386714.57142858,["x"]=6674.5714285703,},
        },
      },
    },
    [AIRBASE.Caucasus.Krymsk] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=293522.00000001,["x"]=-7567.4285714297,},
          [2]={["y"]=293578.57142858,["x"]=-7616.0000000011,},
          [3]={["y"]=295246.00000001,["x"]=-5591.142857144,},
          [4]={["y"]=295187.71428573,["x"]=-5546.0000000011,},
          [5]={["y"]=293523.14285715,["x"]=-7568.2857142868,},
        },
      },
    },
    [AIRBASE.Caucasus.Kutaisi] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=682638,["x"]=-285202.28571429,},
          [2]={["y"]=685050.28571429,["x"]=-284507.42857144,},
          [3]={["y"]=685068.85714286,["x"]=-284578.85714286,},
          [4]={["y"]=682657.42857143,["x"]=-285264.28571429,},
          [5]={["y"]=682638.28571429,["x"]=-285202.85714286,},
        },
      },
    },
    [AIRBASE.Caucasus.Maykop_Khanskaya] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=457005.42857143,["x"]=-27668.000000001,},
          [2]={["y"]=459028.85714286,["x"]=-25168.857142858,},
          [3]={["y"]=459082.57142857,["x"]=-25216.857142858,},
          [4]={["y"]=457060,["x"]=-27714.285714287,},
          [5]={["y"]=457004.57142857,["x"]=-27669.714285715,},
        },
      },
    },
    [AIRBASE.Caucasus.Mineralnye_Vody] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=703904,["x"]=-50352.571428573,},
          [2]={["y"]=707596.28571429,["x"]=-52094.571428573,},
          [3]={["y"]=707560.57142858,["x"]=-52161.714285716,},
          [4]={["y"]=703871.71428572,["x"]=-50420.571428573,},
          [5]={["y"]=703902,["x"]=-50352.000000002,},
        },
      },
    },
    [AIRBASE.Caucasus.Mozdok] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=832201.14285715,["x"]=-83699.428571431,},
          [2]={["y"]=832212.57142857,["x"]=-83780.571428574,},
          [3]={["y"]=835730.28571429,["x"]=-83335.714285717,},
          [4]={["y"]=835718.85714286,["x"]=-83246.571428574,},
          [5]={["y"]=832200.57142857,["x"]=-83700.000000002,},
        },
      },
    },
    [AIRBASE.Caucasus.Nalchik] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=759454.28571429,["x"]=-125551.42857143,},
          [2]={["y"]=759492.85714286,["x"]=-125610.85714286,},
          [3]={["y"]=761406.28571429,["x"]=-124304.28571429,},
          [4]={["y"]=761361.14285714,["x"]=-124239.71428572,},
          [5]={["y"]=759456,["x"]=-125552.57142857,},
        },
      },
    },
    [AIRBASE.Caucasus.Novorossiysk] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=278673.14285716,["x"]=-41615.142857144,},
          [2]={["y"]=278625.42857144,["x"]=-41570.571428572,},
          [3]={["y"]=279835.42857144,["x"]=-40226.000000001,},
          [4]={["y"]=279882.2857143,["x"]=-40270.000000001,},
          [5]={["y"]=278672.00000001,["x"]=-41614.857142858,},
        },
      },
    },
    [AIRBASE.Caucasus.Senaki_Kolkhi] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=646060.85714285,["x"]=-281736,},
          [2]={["y"]=646056.57142857,["x"]=-281631.71428571,},
          [3]={["y"]=648442.28571428,["x"]=-281840.28571428,},
          [4]={["y"]=648432.28571428,["x"]=-281918.85714286,},
          [5]={["y"]=646063.71428571,["x"]=-281738.85714286,},
        },
      },
    },
    [AIRBASE.Caucasus.Sochi_Adler] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=460831.42857143,["x"]=-165180,},
          [2]={["y"]=460878.57142857,["x"]=-165257.14285714,},
          [3]={["y"]=463663.71428571,["x"]=-163793.14285714,},
          [4]={["y"]=463612.28571428,["x"]=-163697.42857143,},
          [5]={["y"]=460831.42857143,["x"]=-165177.14285714,},
        },
        [2] = {
          [1]={["y"]=460831.42857143,["x"]=-165180,},
          [2]={["y"]=460878.57142857,["x"]=-165257.14285714,},
          [3]={["y"]=463663.71428571,["x"]=-163793.14285714,},
          [4]={["y"]=463612.28571428,["x"]=-163697.42857143,},
          [5]={["y"]=460831.42857143,["x"]=-165177.14285714,},
        },
      },
    },
    [AIRBASE.Caucasus.Soganlug] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=894525.71428571,["x"]=-316964,},
          [2]={["y"]=896363.14285714,["x"]=-318634.28571428,},
          [3]={["y"]=896299.14285714,["x"]=-318702.85714286,},
          [4]={["y"]=894464,["x"]=-317031.71428571,},
          [5]={["y"]=894524.57142857,["x"]=-316963.71428571,},
        },
      },
    },
    [AIRBASE.Caucasus.Sukhumi_Babushara] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=562684,["x"]=-219779.71428571,},
          [2]={["y"]=562717.71428571,["x"]=-219718,},
          [3]={["y"]=566046.85714286,["x"]=-221376.57142857,},
          [4]={["y"]=566012.28571428,["x"]=-221446.57142857,},
          [5]={["y"]=562684.57142857,["x"]=-219782.57142857,},
        },
      },
    },
    [AIRBASE.Caucasus.Tbilisi_Lochini] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=895261.14285715,["x"]=-314652.28571428,},
          [2]={["y"]=897654.57142857,["x"]=-316523.14285714,},
          [3]={["y"]=897711.71428571,["x"]=-316450.28571429,},
          [4]={["y"]=895327.42857143,["x"]=-314568.85714286,},
          [5]={["y"]=895261.71428572,["x"]=-314656,},
        },
        [2] = {
          [1]={["y"]=895605.71428572,["x"]=-314724.57142857,},
          [2]={["y"]=897639.71428572,["x"]=-316148,},
          [3]={["y"]=897683.42857143,["x"]=-316087.14285714,},
          [4]={["y"]=895650,["x"]=-314660,},
          [5]={["y"]=895606,["x"]=-314724.85714286,}
        },
      },
    },
    [AIRBASE.Caucasus.Vaziani] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=902239.14285714,["x"]=-318190.85714286,},
          [2]={["y"]=904014.28571428,["x"]=-319994.57142857,},
          [3]={["y"]=904064.85714285,["x"]=-319945.14285715,},
          [4]={["y"]=902294.57142857,["x"]=-318146,},
          [5]={["y"]=902247.71428571,["x"]=-318190.85714286,},
        },
      },
    },
  },
}

--- 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:New( self.Airbases, AirbaseNames ) )

  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 )

  self:SetKickSpeedKmph( 50 )
  self:SetMaximumKickSpeedKmph( 150 )

  --    -- AnapaVityazevo
  --    local AnapaVityazevoBoundary = GROUP:FindByName( "AnapaVityazevo Boundary" )
  --    self.Airbases.AnapaVityazevo.ZoneBoundary = ZONE_POLYGON:New( "AnapaVityazevo Boundary", AnapaVityazevoBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --  
  --    local AnapaVityazevoRunway1 = GROUP:FindByName( "AnapaVityazevo Runway 1" )
  --    self.Airbases.AnapaVityazevo.ZoneRunways[1] = ZONE_POLYGON:New( "AnapaVityazevo Runway 1", AnapaVityazevoRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --  
  --
  --
  --    -- Batumi
  --    local BatumiBoundary = GROUP:FindByName( "Batumi Boundary" )
  --    self.Airbases.Batumi.ZoneBoundary = ZONE_POLYGON:New( "Batumi Boundary", BatumiBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local BatumiRunway1 = GROUP:FindByName( "Batumi Runway 1" )
  --    self.Airbases.Batumi.ZoneRunways[1] = ZONE_POLYGON:New( "Batumi Runway 1", BatumiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Beslan
  --    local BeslanBoundary = GROUP:FindByName( "Beslan Boundary" )
  --    self.Airbases.Beslan.ZoneBoundary = ZONE_POLYGON:New( "Beslan Boundary", BeslanBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local BeslanRunway1 = GROUP:FindByName( "Beslan Runway 1" )
  --    self.Airbases.Beslan.ZoneRunways[1] = ZONE_POLYGON:New( "Beslan Runway 1", BeslanRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Gelendzhik
  --    local GelendzhikBoundary = GROUP:FindByName( "Gelendzhik Boundary" )
  --    self.Airbases.Gelendzhik.ZoneBoundary = ZONE_POLYGON:New( "Gelendzhik Boundary", GelendzhikBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local GelendzhikRunway1 = GROUP:FindByName( "Gelendzhik Runway 1" )
  --    self.Airbases.Gelendzhik.ZoneRunways[1] = ZONE_POLYGON:New( "Gelendzhik Runway 1", GelendzhikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Gudauta
  --    local GudautaBoundary = GROUP:FindByName( "Gudauta Boundary" )
  --    self.Airbases.Gudauta.ZoneBoundary = ZONE_POLYGON:New( "Gudauta Boundary", GudautaBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local GudautaRunway1 = GROUP:FindByName( "Gudauta Runway 1" )
  --    self.Airbases.Gudauta.ZoneRunways[1] = ZONE_POLYGON:New( "Gudauta Runway 1", GudautaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Kobuleti
  --    local KobuletiBoundary = GROUP:FindByName( "Kobuleti Boundary" )
  --    self.Airbases.Kobuleti.ZoneBoundary = ZONE_POLYGON:New( "Kobuleti Boundary", KobuletiBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local KobuletiRunway1 = GROUP:FindByName( "Kobuleti Runway 1" )
  --    self.Airbases.Kobuleti.ZoneRunways[1] = ZONE_POLYGON:New( "Kobuleti Runway 1", KobuletiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- KrasnodarCenter
  --    local KrasnodarCenterBoundary = GROUP:FindByName( "KrasnodarCenter Boundary" )
  --    self.Airbases.KrasnodarCenter.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarCenter Boundary", KrasnodarCenterBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local KrasnodarCenterRunway1 = GROUP:FindByName( "KrasnodarCenter Runway 1" )
  --    self.Airbases.KrasnodarCenter.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarCenter Runway 1", KrasnodarCenterRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- KrasnodarPashkovsky
  --    local KrasnodarPashkovskyBoundary = GROUP:FindByName( "KrasnodarPashkovsky Boundary" )
  --    self.Airbases.KrasnodarPashkovsky.ZoneBoundary = ZONE_POLYGON:New( "KrasnodarPashkovsky Boundary", KrasnodarPashkovskyBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local KrasnodarPashkovskyRunway1 = GROUP:FindByName( "KrasnodarPashkovsky Runway 1" )
  --    self.Airbases.KrasnodarPashkovsky.ZoneRunways[1] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 1", KrasnodarPashkovskyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --    local KrasnodarPashkovskyRunway2 = GROUP:FindByName( "KrasnodarPashkovsky Runway 2" )
  --    self.Airbases.KrasnodarPashkovsky.ZoneRunways[2] = ZONE_POLYGON:New( "KrasnodarPashkovsky Runway 2", KrasnodarPashkovskyRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Krymsk
  --    local KrymskBoundary = GROUP:FindByName( "Krymsk Boundary" )
  --    self.Airbases.Krymsk.ZoneBoundary = ZONE_POLYGON:New( "Krymsk Boundary", KrymskBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local KrymskRunway1 = GROUP:FindByName( "Krymsk Runway 1" )
  --    self.Airbases.Krymsk.ZoneRunways[1] = ZONE_POLYGON:New( "Krymsk Runway 1", KrymskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Kutaisi
  --    local KutaisiBoundary = GROUP:FindByName( "Kutaisi Boundary" )
  --    self.Airbases.Kutaisi.ZoneBoundary = ZONE_POLYGON:New( "Kutaisi Boundary", KutaisiBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local KutaisiRunway1 = GROUP:FindByName( "Kutaisi Runway 1" )
  --    self.Airbases.Kutaisi.ZoneRunways[1] = ZONE_POLYGON:New( "Kutaisi Runway 1", KutaisiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- MaykopKhanskaya
  --    local MaykopKhanskayaBoundary = GROUP:FindByName( "MaykopKhanskaya Boundary" )
  --    self.Airbases.MaykopKhanskaya.ZoneBoundary = ZONE_POLYGON:New( "MaykopKhanskaya Boundary", MaykopKhanskayaBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local MaykopKhanskayaRunway1 = GROUP:FindByName( "MaykopKhanskaya Runway 1" )
  --    self.Airbases.MaykopKhanskaya.ZoneRunways[1] = ZONE_POLYGON:New( "MaykopKhanskaya Runway 1", MaykopKhanskayaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- MineralnyeVody
  --    local MineralnyeVodyBoundary = GROUP:FindByName( "MineralnyeVody Boundary" )
  --    self.Airbases.MineralnyeVody.ZoneBoundary = ZONE_POLYGON:New( "MineralnyeVody Boundary", MineralnyeVodyBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local MineralnyeVodyRunway1 = GROUP:FindByName( "MineralnyeVody Runway 1" )
  --    self.Airbases.MineralnyeVody.ZoneRunways[1] = ZONE_POLYGON:New( "MineralnyeVody Runway 1", MineralnyeVodyRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Mozdok
  --    local MozdokBoundary = GROUP:FindByName( "Mozdok Boundary" )
  --    self.Airbases.Mozdok.ZoneBoundary = ZONE_POLYGON:New( "Mozdok Boundary", MozdokBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local MozdokRunway1 = GROUP:FindByName( "Mozdok Runway 1" )
  --    self.Airbases.Mozdok.ZoneRunways[1] = ZONE_POLYGON:New( "Mozdok Runway 1", MozdokRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Nalchik
  --    local NalchikBoundary = GROUP:FindByName( "Nalchik Boundary" )
  --    self.Airbases.Nalchik.ZoneBoundary = ZONE_POLYGON:New( "Nalchik Boundary", NalchikBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local NalchikRunway1 = GROUP:FindByName( "Nalchik Runway 1" )
  --    self.Airbases.Nalchik.ZoneRunways[1] = ZONE_POLYGON:New( "Nalchik Runway 1", NalchikRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Novorossiysk
  --    local NovorossiyskBoundary = GROUP:FindByName( "Novorossiysk Boundary" )
  --    self.Airbases.Novorossiysk.ZoneBoundary = ZONE_POLYGON:New( "Novorossiysk Boundary", NovorossiyskBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local NovorossiyskRunway1 = GROUP:FindByName( "Novorossiysk Runway 1" )
  --    self.Airbases.Novorossiysk.ZoneRunways[1] = ZONE_POLYGON:New( "Novorossiysk Runway 1", NovorossiyskRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- SenakiKolkhi
  --    local SenakiKolkhiBoundary = GROUP:FindByName( "SenakiKolkhi Boundary" )
  --    self.Airbases.SenakiKolkhi.ZoneBoundary = ZONE_POLYGON:New( "SenakiKolkhi Boundary", SenakiKolkhiBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local SenakiKolkhiRunway1 = GROUP:FindByName( "SenakiKolkhi Runway 1" )
  --    self.Airbases.SenakiKolkhi.ZoneRunways[1] = ZONE_POLYGON:New( "SenakiKolkhi Runway 1", SenakiKolkhiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- SochiAdler
  --    local SochiAdlerBoundary = GROUP:FindByName( "SochiAdler Boundary" )
  --    self.Airbases.SochiAdler.ZoneBoundary = ZONE_POLYGON:New( "SochiAdler Boundary", SochiAdlerBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local SochiAdlerRunway1 = GROUP:FindByName( "SochiAdler Runway 1" )
  --    self.Airbases.SochiAdler.ZoneRunways[1] = ZONE_POLYGON:New( "SochiAdler Runway 1", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --    local SochiAdlerRunway2 = GROUP:FindByName( "SochiAdler Runway 2" )
  --    self.Airbases.SochiAdler.ZoneRunways[2] = ZONE_POLYGON:New( "SochiAdler Runway 2", SochiAdlerRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Soganlug
  --    local SoganlugBoundary = GROUP:FindByName( "Soganlug Boundary" )
  --    self.Airbases.Soganlug.ZoneBoundary = ZONE_POLYGON:New( "Soganlug Boundary", SoganlugBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local SoganlugRunway1 = GROUP:FindByName( "Soganlug Runway 1" )
  --    self.Airbases.Soganlug.ZoneRunways[1] = ZONE_POLYGON:New( "Soganlug Runway 1", SoganlugRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- SukhumiBabushara
  --    local SukhumiBabusharaBoundary = GROUP:FindByName( "SukhumiBabushara Boundary" )
  --    self.Airbases.SukhumiBabushara.ZoneBoundary = ZONE_POLYGON:New( "SukhumiBabushara Boundary", SukhumiBabusharaBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local SukhumiBabusharaRunway1 = GROUP:FindByName( "SukhumiBabushara Runway 1" )
  --    self.Airbases.SukhumiBabushara.ZoneRunways[1] = ZONE_POLYGON:New( "SukhumiBabushara Runway 1", SukhumiBabusharaRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- TbilisiLochini
  --    local TbilisiLochiniBoundary = GROUP:FindByName( "TbilisiLochini Boundary" )
  --    self.Airbases.TbilisiLochini.ZoneBoundary = ZONE_POLYGON:New( "TbilisiLochini Boundary", TbilisiLochiniBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --  
  --    local TbilisiLochiniRunway1 = GROUP:FindByName( "TbilisiLochini Runway 1" )
  --    self.Airbases.TbilisiLochini.ZoneRunways[1] = ZONE_POLYGON:New( "TbilisiLochini Runway 1", TbilisiLochiniRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --      
  --    local TbilisiLochiniRunway2 = GROUP:FindByName( "TbilisiLochini Runway 2" )
  --    self.Airbases.TbilisiLochini.ZoneRunways[2] = ZONE_POLYGON:New( "TbilisiLochini Runway 2", TbilisiLochiniRunway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --
  --    -- Vaziani
  --    local VazianiBoundary = GROUP:FindByName( "Vaziani Boundary" )
  --    self.Airbases.Vaziani.ZoneBoundary = ZONE_POLYGON:New( "Vaziani Boundary", VazianiBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --
  --    local VazianiRunway1 = GROUP:FindByName( "Vaziani Runway 1" )
  --    self.Airbases.Vaziani.ZoneRunways[1] = ZONE_POLYGON:New( "Vaziani Runway 1", VazianiRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  --
  --
  --


        -- Template
  --    local TemplateBoundary = GROUP:FindByName( "Template Boundary" )
  --    self.Airbases.Template.ZoneBoundary = ZONE_POLYGON:New( "Template Boundary", TemplateBoundary ):SmokeZone(SMOKECOLOR.White):Flush()
  --  
  --    local TemplateRunway1 = GROUP:FindByName( "Template Runway 1" )
  --    self.Airbases.Template.ZoneRunways[1] = ZONE_POLYGON:New( "Template Runway 1", TemplateRunway1 ):SmokeZone(SMOKECOLOR.Red):Flush()

  return self
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_Airport`
--    * `AIRBASE.Nevada.Boulder_City_Airport`
--    * `AIRBASE.Nevada.Creech_AFB`
--    * `AIRBASE.Nevada.Echo_Bay`
--    * `AIRBASE.Nevada.Groom_Lake_AFB`
--    * `AIRBASE.Nevada.Henderson_Executive_Airport`
--    * `AIRBASE.Nevada.Jean_Airport`
--    * `AIRBASE.Nevada.Laughlin_Airport`
--    * `AIRBASE.Nevada.Lincoln_County`
--    * `AIRBASE.Nevada.McCarran_International_Airport`
--    * `AIRBASE.Nevada.Mellan_Airstrip`
--    * `AIRBASE.Nevada.Mesquite`
--    * `AIRBASE.Nevada.Mina_Airport_3Q0`
--    * `AIRBASE.Nevada.Nellis_AFB`
--    * `AIRBASE.Nevada.North_Las_Vegas`
--    * `AIRBASE.Nevada.Pahute_Mesa_Airstrip`
--    * `AIRBASE.Nevada.Tonopah_Airport`
--    * `AIRBASE.Nevada.Tonopah_Test_Range_Airfield`
--
-- # 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_Airport,             
--         AIRBASE.Nevada.Mellan_Airstrip,              
--         AIRBASE.Nevada.Lincoln_County,               
--         AIRBASE.Nevada.North_Las_Vegas,              
--         AIRBASE.Nevada.McCarran_International_Airport
--       }                                              
--     )                                                
-- 
-- ## 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",
  Airbases = {

    [AIRBASE.Nevada.Beatty_Airport] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-174950.05857143,["x"]=-329679.65,},
          [2]={["y"]=-174946.53828571,["x"]=-331394.03885715,},
          [3]={["y"]=-174967.10971429,["x"]=-331394.32457143,},
          [4]={["y"]=-174971.01828571,["x"]=-329682.59171429,},
        },
      },
    },  
    [AIRBASE.Nevada.Boulder_City_Airport] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-1317.841714286,["x"]=-429014.92857142,},
          [2] = {["y"]=-951.26228571458,["x"]=-430310.21142856,},
          [3] = {["y"]=-978.11942857172,["x"]=-430317.06857142,},
          [4] = {["y"]=-1347.5088571432,["x"]=-429023.98485713,},
        },
        [2] = {
          [1] = {["y"]=-1879.955714286,["x"]=-429783.83742856,},
          [2] = {["y"]=-256.25257142886,["x"]=-430023.63542856,},
          [3] = {["y"]=-260.25257142886,["x"]=-430048.77828571,},
          [4] = {["y"]=-1883.955714286,["x"]=-429807.83742856,},
        },
      },
    },
    [AIRBASE.Nevada.Creech_AFB] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-74234.729142857,["x"]=-360501.80857143,},
          [2] = {["y"]=-77606.122285714,["x"]=-360417.86542857,},
          [3] = {["y"]=-77608.578,["x"]=-360486.13428571,},
          [4] = {["y"]=-74237.930571428,["x"]=-360586.25628571,},
        },
        [2] = {
          [1] = {["y"]=-75807.571428572,["x"]=-359073.42857142,},
          [2] = {["y"]=-74770.142857144,["x"]=-360581.71428571,},
          [3] = {["y"]=-74641.285714287,["x"]=-360585.42857142,},
          [4] = {["y"]=-75734.142857144,["x"]=-359023.14285714,},
        },
      },
    },
    [AIRBASE.Nevada.Echo_Bay] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=33182.919428572,["x"]=-388698.21657142,},
          [2] = {["y"]=34202.543142857,["x"]=-388469.55485714,},
          [3] = {["y"]=34207.686,["x"]=-388488.69771428,},
          [4] = {["y"]=33185.422285715,["x"]=-388717.82228571,},
        },
      },
    },
    [AIRBASE.Nevada.Groom_Lake_AFB] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-85971.465428571,["x"]=-290567.77,},
          [2] = {["y"]=-87691.155428571,["x"]=-286637.75428571,},
          [3] = {["y"]=-87756.714285715,["x"]=-286663.99999999,},
          [4] = {["y"]=-86035.940285714,["x"]=-290598.81314286,},
        },
        [2] = {
          [1] = {["y"]=-86741.547142857,["x"]=-290353.31971428,},
          [2] = {["y"]=-89672.714285714,["x"]=-283546.57142855,},
          [3] = {["y"]=-89772.142857143,["x"]=-283587.71428569,},
          [4] = {["y"]=-86799.623714285,["x"]=-290374.16771428,},
        },
      },
    },
    [AIRBASE.Nevada.Henderson_Executive_Airport] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-25837.500571429,["x"]=-426404.25257142,},
          [2] = {["y"]=-25843.509428571,["x"]=-428752.67942856,},
          [3] = {["y"]=-25902.343714286,["x"]=-428749.96399999,},
          [4] = {["y"]=-25934.667142857,["x"]=-426411.45657142,},
        },
        [2] = {
          [1] = {["y"]=-25650.296285714,["x"]=-426510.17971428,},
          [2] = {["y"]=-25632.443428571,["x"]=-428297.11428571,},
          [3] = {["y"]=-25686.690285714,["x"]=-428299.37457142,},
          [4] = {["y"]=-25708.296285714,["x"]=-426515.15114285,},
        },
      },
    },
    [AIRBASE.Nevada.Jean_Airport] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-42549.187142857,["x"]=-449663.23257143,},
          [2] = {["y"]=-43367.466285714,["x"]=-451044.77657143,},
          [3] = {["y"]=-43395.180571429,["x"]=-451028.20514286,},
          [4] = {["y"]=-42579.893142857,["x"]=-449648.18371428,},
        },
        [2] = {
          [1] = {["y"]=-42588.359428572,["x"]=-449900.14342857,},
          [2] = {["y"]=-43349.698285714,["x"]=-451185.46857143,},
          [3] = {["y"]=-43369.624571429,["x"]=-451173.49342857,},
          [4] = {["y"]=-42609.216571429,["x"]=-449891.28628571,},
        },
      },
    },
    [AIRBASE.Nevada.Laughlin_Airport] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=28231.600857143,["x"]=-515555.94114286,},
          [2] = {["y"]=28453.728285714,["x"]=-518170.78885714,},
          [3] = {["y"]=28370.788285714,["x"]=-518176.25742857,},
          [4] = {["y"]=28138.022857143,["x"]=-515573.07514286,},
        },
        [2] = {
          [1] = {["y"]=28231.600857143,["x"]=-515555.94114286,},
          [2] = {["y"]=28453.728285714,["x"]=-518170.78885714,},
          [3] = {["y"]=28370.788285714,["x"]=-518176.25742857,},
          [4] = {["y"]=28138.022857143,["x"]=-515573.07514286,},
        },
      },
    },
    [AIRBASE.Nevada.Lincoln_County] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=33222.34171429,["x"]=-223959.40171429,},
          [2]={["y"]=33200.040000004,["x"]=-225369.36828572,},
          [3]={["y"]=33177.634571428,["x"]=-225369.21485715,},
          [4]={["y"]=33201.198857147,["x"]=-223960.54457143,},
        },
      },
    },
    [AIRBASE.Nevada.McCarran_International_Airport] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-29406.035714286,["x"]=-416102.48199999,},
          [2] = {["y"]=-24680.714285715,["x"]=-416003.14285713,},
          [3] = {["y"]=-24681.857142858,["x"]=-415926.57142856,},
          [4] = {["y"]=-29408.42857143,["x"]=-416016.57142856,},
        },
        [2] = {
          [1] = {["y"]=-28567.221714286,["x"]=-416378.61799999,},
          [2] = {["y"]=-25109.912285714,["x"]=-416309.92914285,},
          [3] = {["y"]=-25112.508,["x"]=-416240.78714285,},
          [4] = {["y"]=-28576.247428571,["x"]=-416308.49514285,},
        },
        [3] = {
          [1] = {["y"]=-29255.953142857,["x"]=-416307.10657142,},
          [2] = {["y"]=-28005.571428572,["x"]=-413449.7142857,},
          [3] = {["y"]=-28068.714285715,["x"]=-413422.85714284,},
          [4] = {["y"]=-29331.000000001,["x"]=-416275.7142857,},
        },
        [4] = {
          [1] = {["y"]=-28994.901714286,["x"]=-416423.0522857,},
          [2] = {["y"]=-27697.571428572,["x"]=-413464.57142856,},
          [3] = {["y"]=-27767.857142858,["x"]=-413434.28571427,},
          [4] = {["y"]=-29073.000000001,["x"]=-416386.85714284,},
        },
      },
    },
    [AIRBASE.Nevada.Mesquite] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=68188.340285714,["x"]=-330302.54742857,},
          [2] = {["y"]=68911.303428571,["x"]=-328920.76571429,},
          [3] = {["y"]=68936.927142857,["x"]=-328933.888,},
          [4] = {["y"]=68212.460285714,["x"]=-330317.19171429,},
        },
      },
    },
    [AIRBASE.Nevada.Mina_Airport_3Q0] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-290054.57371429,["x"]=-160930.02228572,},
          [2] = {["y"]=-289469.77457143,["x"]=-162048.73571429,},
          [3] = {["y"]=-289520.06028572,["x"]=-162074.73571429,},
          [4] = {["y"]=-290104.69085714,["x"]=-160956.19457143,},
        },
      },
    },
    [AIRBASE.Nevada.Nellis_AFB] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-18614.218571428,["x"]=-399437.91085714,},
          [2] = {["y"]=-16217.857142857,["x"]=-396596.85714286,},
          [3] = {["y"]=-16300.142857143,["x"]=-396530,},
          [4] = {["y"]=-18692.543428571,["x"]=-399381.31114286,},
        },
        [2] = {
          [1] = {["y"]=-18388.948857143,["x"]=-399630.51828571,},
          [2] = {["y"]=-16011,["x"]=-396806.85714286,},
          [3] = {["y"]=-16074.714285714,["x"]=-396751.71428572,},
          [4] = {["y"]=-18451.571428572,["x"]=-399580.85714285,},
        },
      },
    },
    [AIRBASE.Nevada.Pahute_Mesa_Airstrip] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-132690.40942857,["x"]=-302733.53085714,},
          [2] = {["y"]=-133112.43228571,["x"]=-304499.70742857,},
          [3] = {["y"]=-133179.91685714,["x"]=-304485.544,},
          [4] = {["y"]=-132759.988,["x"]=-302723.326,},
        },
      },
    },
    [AIRBASE.Nevada.Tonopah_Test_Range_Airfield] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-175389.162,["x"]=-224778.07685715,},
          [2] = {["y"]=-173942.15485714,["x"]=-228210.27571429,},
          [3] = {["y"]=-174001.77085714,["x"]=-228233.60371429,},
          [4] = {["y"]=-175452.38685714,["x"]=-224806.84200001,},
        },
      },
    },
    [AIRBASE.Nevada.Tonopah_Airport] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-202128.25228571,["x"]=-196701.34314286,},
          [2] = {["y"]=-201562.40828571,["x"]=-198814.99714286,},
          [3] = {["y"]=-201591.44828571,["x"]=-198820.93714286,},
          [4] = {["y"]=-202156.06828571,["x"]=-196707.68714286,},
        },
        [2] = {
          [1] = {["y"]=-202084.57171428,["x"]=-196722.02228572,},
          [2] = {["y"]=-200592.75485714,["x"]=-197768.05571429,},
          [3] = {["y"]=-200605.37285714,["x"]=-197783.49228572,},
          [4] = {["y"]=-202097.14314285,["x"]=-196739.16514286,},
        },
      },
    },
    [AIRBASE.Nevada.North_Las_Vegas] = {
      PointsRunways = {
        [1] = {
          [1] = {["y"]=-32599.017714286,["x"]=-400913.26485714,},
          [2] = {["y"]=-30881.068857143,["x"]=-400837.94628571,},
          [3] = {["y"]=-30879.354571428,["x"]=-400873.08914285,},
          [4] = {["y"]=-32595.966285714,["x"]=-400947.13571428,},
        },
        [2] = {
          [1] = {["y"]=-32499.448571428,["x"]=-400690.99514285,},
          [2] = {["y"]=-31247.514857143,["x"]=-401868.95571428,},
          [3] = {["y"]=-31271.802857143,["x"]=-401894.97857142,},
          [4] = {["y"]=-32520.02,["x"]=-400716.99514285,},
        },
        [3] = {
          [1] = {["y"]=-31865.254857143,["x"]=-400999.74057143,},
          [2] = {["y"]=-30893.604,["x"]=-401908.85742857,},
          [3] = {["y"]=-30915.578857143,["x"]=-401936.03685714,},
          [4] = {["y"]=-31884.969142858,["x"]=-401020.59771429,},
        },
      },
    },
  },
}

--- 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:New( self.Airbases, AirbaseNames ) )

  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 )

  self:SetKickSpeedKmph( 50 )
  self:SetMaximumKickSpeedKmph( 150 )

  -- These lines here are for the demonstration mission.
  -- They create in the dcs.log the coordinates of the runway polygons, that are then
  -- taken by the moose designer from the dcs.log and reworked to define the
  -- Airbases structure, which is part of the class.
  -- When new airbases are added or airbases are changed on the map,
  -- the MOOSE designer willde-comment this section and apply the changes in the demo
  -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates
  -- in the Airbases structure.
  -- So, this needs to stay commented normally once a map has been finished.

  --[[
  
  -- Beatty
  do 
    local VillagePrefix = "Beatty" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  -- Boulder
  do 
    local VillagePrefix = "Boulder" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  -- Creech
  do
    local VillagePrefix = "Creech" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Echo
  do 
    local VillagePrefix = "Echo" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Groom Lake
  do
    local VillagePrefix = "GroomLake" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  -- Henderson
  do 
    local VillagePrefix = "Henderson" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  -- Jean
  do 
    local VillagePrefix = "Jean" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Laughlin
  do 
    local VillagePrefix = "Laughlin" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  -- Lincoln
  do 
    local VillagePrefix = "Lincoln" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- McCarran
  do
    local VillagePrefix = "McCarran" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway3 = GROUP:FindByName( VillagePrefix .. " 3" )
    local Zone3 = ZONE_POLYGON:New( VillagePrefix .. " 3", Runway3 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway4 = GROUP:FindByName( VillagePrefix .. " 4" )
    local Zone4 = ZONE_POLYGON:New( VillagePrefix .. " 4", Runway4 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Mesquite
  do 
    local VillagePrefix = "Mesquite" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Mina
  do 
    local VillagePrefix = "Mina" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Nellis
  do
    local VillagePrefix = "Nellis" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  -- Pahute
  do 
    local VillagePrefix = "Pahute" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- TonopahTR
  do 
    local VillagePrefix = "TonopahTR" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Tonopah
  do 
    local VillagePrefix = "Tonopah" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end

  -- Vegas
  do 
    local VillagePrefix = "Vegas" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway3 = GROUP:FindByName( VillagePrefix .. " 3" )
    local Zone3 = ZONE_POLYGON:New( VillagePrefix .. " 3", Runway3 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  --]]
  
  return self
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 Caucasus 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`
--
-- # 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",
  Airbases = {
    [AIRBASE.Normandy.Azeville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-74194.387714285,["x"]=-2691.1399999998,},
          [2]={["y"]=-73160.282571428,["x"]=-2310.0274285712,},
          [3]={["y"]=-73141.711142857,["x"]=-2357.7417142855,},
          [4]={["y"]=-74176.959142857,["x"]=-2741.997142857,},
        },
      },
    },
    [AIRBASE.Normandy.Bazenville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-19246.209999999,["x"]=-21246.748,},
          [2]={["y"]=-17883.70142857,["x"]=-20219.009714285,},
          [3]={["y"]=-17855.415714285,["x"]=-20256.438285714,},
          [4]={["y"]=-19217.791999999,["x"]=-21283.597714285,},
        },
      },
    },
    [AIRBASE.Normandy.Beny_sur_Mer] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-8592.7442857133,["x"]=-20386.15542857,},
          [2]={["y"]=-8404.4931428561,["x"]=-21744.113142856,},
          [3]={["y"]=-8267.9917142847,["x"]=-21724.97742857,},
          [4]={["y"]=-8451.0482857133,["x"]=-20368.87542857,},
        },
      },
    },
    [AIRBASE.Normandy.Beuzeville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-71552.573428571,["x"]=-8744.3688571427,},
          [2]={["y"]=-72577.765714285,["x"]=-9638.5682857141,},
          [3]={["y"]=-72609.304285714,["x"]=-9601.2954285712,},
          [4]={["y"]=-71585.849428571,["x"]=-8709.9648571426,},
        },
      },
    },
    [AIRBASE.Normandy.Biniville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-84757.320285714,["x"]=-7377.1354285713,},
          [2]={["y"]=-84271.482,["x"]=-7956.4859999999,},
          [3]={["y"]=-84299.482,["x"]=-7981.6288571427,},
          [4]={["y"]=-84784.969714286,["x"]=-7402.0588571427,},
        },
      },
    },
    [AIRBASE.Normandy.Brucheville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-65546.792857142,["x"]=-14615.640857143,},
          [2]={["y"]=-66914.692,["x"]=-15232.713714285,},
          [3]={["y"]=-66896.527714285,["x"]=-15271.948571428,},
          [4]={["y"]=-65528.393714285,["x"]=-14657.995714286,},
        },
      },
    },
    [AIRBASE.Normandy.Cardonville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-54280.445428571,["x"]=-15843.749142857,},
          [2]={["y"]=-53646.998571428,["x"]=-17143.012285714,},
          [3]={["y"]=-53683.93,["x"]=-17161.317428571,},
          [4]={["y"]=-54323.354571428,["x"]=-15855.004,},
        },
      },
    },
    [AIRBASE.Normandy.Carpiquet] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-10751.325714285,["x"]=-34229.494,},
          [2]={["y"]=-9283.5279999993,["x"]=-35192.352857142,},
          [3]={["y"]=-9325.2005714274,["x"]=-35260.967714285,},
          [4]={["y"]=-10794.90942857,["x"]=-34287.041428571,},
        },
      },
    },
    [AIRBASE.Normandy.Chailey] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=12895.585714292,["x"]=164683.05657144,},
          [2]={["y"]=11410.727142863,["x"]=163606.54485715,},
          [3]={["y"]=11363.012857149,["x"]=163671.97342858,},
          [4]={["y"]=12797.537142863,["x"]=164711.01857144,},
          [5]={["y"]=12862.902857149,["x"]=164726.99685715,},
        },
        [2] = {
          [1]={["y"]=11805.316000006,["x"]=164502.90971429,},
          [2]={["y"]=11997.280857149,["x"]=163032.65542858,},
          [3]={["y"]=11918.640857149,["x"]=163023.04657144,},
          [4]={["y"]=11726.973428578,["x"]=164489.94257143,},
        },
      },
    },
    [AIRBASE.Normandy.Chippelle] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-48540.313999999,["x"]=-28884.795999999,},
          [2]={["y"]=-47251.820285713,["x"]=-28140.128571427,},
          [3]={["y"]=-47274.551714285,["x"]=-28103.758285713,},
          [4]={["y"]=-48555.657714285,["x"]=-28839.90142857,},
        },
      },
    },
    [AIRBASE.Normandy.Cretteville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-78351.723142857,["x"]=-18177.725428571,},
          [2]={["y"]=-77220.322285714,["x"]=-19125.687714286,},
          [3]={["y"]=-77247.899428571,["x"]=-19158.49,},
          [4]={["y"]=-78380.008857143,["x"]=-18208.011142857,},
        },
      },
    },
    [AIRBASE.Normandy.Cricqueville_en_Bessin] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-50875.034571428,["x"]=-14322.404571428,},
          [2]={["y"]=-50681.148571428,["x"]=-15825.258,},
          [3]={["y"]=-50717.434285713,["x"]=-15829.829428571,},
          [4]={["y"]=-50910.569428571,["x"]=-14327.562857142,},
        },
      },
    },
    [AIRBASE.Normandy.Deux_Jumeaux] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-49575.410857142,["x"]=-16575.161142857,},
          [2]={["y"]=-48149.077999999,["x"]=-16952.193428571,},
          [3]={["y"]=-48159.935142856,["x"]=-16996.764857142,},
          [4]={["y"]=-49584.839428571,["x"]=-16617.732571428,},
        },
      },
    },
    [AIRBASE.Normandy.Evreux] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=112906.84828572,["x"]=-45585.824857142,},
          [2]={["y"]=112050.38228572,["x"]=-46811.871999999,},
          [3]={["y"]=111980.05371429,["x"]=-46762.173142856,},
          [4]={["y"]=112833.54542857,["x"]=-45540.010571428,},
        },
        [2] = {
          [1]={["y"]=112046.02085714,["x"]=-45091.056571428,},
          [2]={["y"]=112488.668,["x"]=-46623.617999999,},
          [3]={["y"]=112405.66914286,["x"]=-46647.419142856,},
          [4]={["y"]=111966.03657143,["x"]=-45112.604285713,},
        },
      },
    },
    [AIRBASE.Normandy.Ford] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-26506.13971428,["x"]=147514.39971429,},
          [2]={["y"]=-25012.977428565,["x"]=147566.14485715,},
          [3]={["y"]=-25009.851428565,["x"]=147482.63600001,},
          [4]={["y"]=-26503.693999994,["x"]=147427.33228572,},
        },
        [2] = {
          [1]={["y"]=-25169.701999994,["x"]=148421.09257143,},
          [2]={["y"]=-26092.421999994,["x"]=147190.89628572,},
          [3]={["y"]=-26158.136285708,["x"]=147240.89628572,},
          [4]={["y"]=-25252.357999994,["x"]=148448.64457143,},
        },
      },
    },
    [AIRBASE.Normandy.Funtington] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-44698.388571423,["x"]=152952.17257143,},
          [2]={["y"]=-46452.993142851,["x"]=152388.77885714,},
          [3]={["y"]=-46476.361142851,["x"]=152470.05885714,},
          [4]={["y"]=-44787.256571423,["x"]=153009.52,},
          [5]={["y"]=-44715.581428566,["x"]=153002.08714286,},
        },
        [2] = {
          [1]={["y"]=-45792.665999994,["x"]=153123.894,},
          [2]={["y"]=-46068.084857137,["x"]=151665.98342857,},
          [3]={["y"]=-46148.632285708,["x"]=151681.58685714,},
          [4]={["y"]=-45871.25971428,["x"]=153136.82714286,},
        },
      },
    },
    [AIRBASE.Normandy.Lantheuil] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-17158.84542857,["x"]=-24602.999428571,},
          [2]={["y"]=-15978.59342857,["x"]=-23922.978571428,},
          [3]={["y"]=-15932.021999999,["x"]=-24004.121428571,},
          [4]={["y"]=-17090.734857142,["x"]=-24673.248,},
        },
      },
    },
    [AIRBASE.Normandy.Lessay] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-87667.304571429,["x"]=-33220.165714286,},
          [2]={["y"]=-86146.607714286,["x"]=-34248.483142857,},
          [3]={["y"]=-86191.538285714,["x"]=-34316.991142857,},
          [4]={["y"]=-87712.212,["x"]=-33291.774857143,},
        },
        [2] = {
          [1]={["y"]=-87125.123142857,["x"]=-34183.682571429,},
          [2]={["y"]=-85803.278285715,["x"]=-33498.428857143,},
          [3]={["y"]=-85768.408285715,["x"]=-33570.13,},
          [4]={["y"]=-87087.688571429,["x"]=-34258.272285715,},
        },
      },
    },
    [AIRBASE.Normandy.Lignerolles] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-35279.611714285,["x"]=-35232.026857142,},
          [2]={["y"]=-33804.948857142,["x"]=-35770.713999999,},
          [3]={["y"]=-33789.876285713,["x"]=-35726.655714284,},
          [4]={["y"]=-35263.548285713,["x"]=-35192.75542857,},
        },
      },
    },
    [AIRBASE.Normandy.Longues_sur_Mer] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-29444.070285713,["x"]=-16334.105428571,},
          [2]={["y"]=-28265.52942857,["x"]=-17011.557999999,},
          [3]={["y"]=-28344.74742857,["x"]=-17143.587999999,},
          [4]={["y"]=-29529.616285713,["x"]=-16477.766571428,},
        },
      },
    },
    [AIRBASE.Normandy.Maupertus] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-85605.340857143,["x"]=16175.267714286,},
          [2]={["y"]=-84132.567142857,["x"]=15895.905714286,},
          [3]={["y"]=-84139.995142857,["x"]=15847.623714286,},
          [4]={["y"]=-85613.626571429,["x"]=16132.410571429,},
        },
      },
    },
    [AIRBASE.Normandy.Meautis] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-72642.527714286,["x"]=-24593.622285714,},
          [2]={["y"]=-71298.672571429,["x"]=-24352.651142857,},
          [3]={["y"]=-71290.101142857,["x"]=-24398.365428571,},
          [4]={["y"]=-72631.715714286,["x"]=-24639.966857143,},
        },
      },
    },
    [AIRBASE.Normandy.Le_Molay] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-41876.526857142,["x"]=-26701.052285713,},
          [2]={["y"]=-40979.545714285,["x"]=-25675.045999999,},
          [3]={["y"]=-41017.687428571,["x"]=-25644.272571427,},
          [4]={["y"]=-41913.638285713,["x"]=-26665.137999999,},
        },
      },
    },
    [AIRBASE.Normandy.Needs_Oar_Point] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-83882.441142851,["x"]=141429.83314286,},
          [2]={["y"]=-85138.159428566,["x"]=140187.52828572,},
          [3]={["y"]=-85208.323428566,["x"]=140161.04371429,},
          [4]={["y"]=-85245.751999994,["x"]=140201.61514286,},
          [5]={["y"]=-83939.966571423,["x"]=141485.22085714,},
        },
        [2] = {
          [1]={["y"]=-84528.76571428,["x"]=141988.01428572,},
          [2]={["y"]=-84116.98971428,["x"]=140565.78685714,},
          [3]={["y"]=-84199.35771428,["x"]=140541.14685714,},
          [4]={["y"]=-84605.051428566,["x"]=141966.01428572,},
        },
      },
    },
    [AIRBASE.Normandy.Picauville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-80808.838571429,["x"]=-11834.554571428,},
          [2]={["y"]=-79531.574285714,["x"]=-12311.274,},
          [3]={["y"]=-79549.355428571,["x"]=-12356.928285714,},
          [4]={["y"]=-80827.815142857,["x"]=-11901.835142857,},
        },
      },
    },
    [AIRBASE.Normandy.Rucqueville] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-20023.988857141,["x"]=-26569.565428571,},
          [2]={["y"]=-18688.92542857,["x"]=-26571.086571428,},
          [3]={["y"]=-18688.012571427,["x"]=-26611.252285713,},
          [4]={["y"]=-20022.218857141,["x"]=-26608.505428571,},
        },
      },
    },
    [AIRBASE.Normandy.Saint_Pierre_du_Mont] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-48015.384571428,["x"]=-11886.631714285,},
          [2]={["y"]=-46540.412285713,["x"]=-11945.226571428,},
          [3]={["y"]=-46541.349999999,["x"]=-11991.174571428,},
          [4]={["y"]=-48016.837142856,["x"]=-11929.371142857,},
        },
      },
    },
    [AIRBASE.Normandy.Sainte_Croix_sur_Mer] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-15877.817999999,["x"]=-18812.579999999,},
          [2]={["y"]=-14464.377142856,["x"]=-18807.46,},
          [3]={["y"]=-14463.879714285,["x"]=-18759.706857142,},
          [4]={["y"]=-15878.229142856,["x"]=-18764.071428571,},
        },
      },
    },
    [AIRBASE.Normandy.Sainte_Laurent_sur_Mer] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-41676.834857142,["x"]=-14475.109428571,},
          [2]={["y"]=-40566.11142857,["x"]=-14817.319999999,},
          [3]={["y"]=-40579.543999999,["x"]=-14860.059999999,},
          [4]={["y"]=-41687.120571427,["x"]=-14509.680857142,},
        },
      },
    },
    [AIRBASE.Normandy.Sommervieu] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-26821.913714284,["x"]=-21390.466571427,},
          [2]={["y"]=-25465.308857142,["x"]=-21296.859999999,},
          [3]={["y"]=-25462.451714284,["x"]=-21343.717142856,},
          [4]={["y"]=-26818.002285713,["x"]=-21440.532857142,},
        },
      },
    },
    [AIRBASE.Normandy.Tangmere] = {
      PointsRunways = {
        [1] = {
          [1]={["y"]=-34684.581142851,["x"]=150459.61657143,},
          [2]={["y"]=-33250.625428566,["x"]=149954.17,},
          [3]={["y"]=-33275.724285708,["x"]=149874.69028572,},
          [4]={["y"]=-34709.020571423,["x"]=150377.93742857,},
        },
        [2] = {
          [1]={["y"]=-33103.438857137,["x"]=150812.72542857,},
          [2]={["y"]=-34410.246285708,["x"]=150009.73142857,},
          [3]={["y"]=-34453.535142851,["x"]=150082.02685714,},
          [4]={["y"]=-33176.545999994,["x"]=150870.22542857,},
        },
      },
    },
  },
}


--- 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:New( self.Airbases, AirbaseNames ) ) -- #ATC_GROUND_NORMANDY

  self.AirbaseMonitor = SCHEDULER:New( self, self._AirbaseMonitor, { self }, 0, 2, 0.05 )
  
  self:SetKickSpeedKmph( 40 )
  self:SetMaximumKickSpeedKmph( 100 )

  -- These lines here are for the demonstration mission.
  -- They create in the dcs.log the coordinates of the runway polygons, that are then
  -- taken by the moose designer from the dcs.log and reworked to define the
  -- Airbases structure, which is part of the class.
  -- When new airbases are added or airbases are changed on the map,
  -- the MOOSE designer willde-comment this section and apply the changes in the demo
  -- mission, and do a re-run to create a new dcs.log, and then add the changed coordinates
  -- in the Airbases structure.
  -- So, this needs to stay commented normally once a map has been finished.
  
  --[[
  
  -- Azeville
  do
    local VillagePrefix = "Azeville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Bazenville
  do
    local VillagePrefix = "Bazenville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Beny
  do
    local VillagePrefix = "Beny" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Beuzeville
  do
    local VillagePrefix = "Beuzeville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Biniville
  do
    local VillagePrefix = "Biniville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Brucheville
  do
    local VillagePrefix = "Brucheville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Cardonville
  do
    local VillagePrefix = "Cardonville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Carpiquet
  do
    local VillagePrefix = "Carpiquet" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Chailey
  do
    local VillagePrefix = "Chailey" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Chippelle
  do
    local VillagePrefix = "Chippelle" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Cretteville
  do
    local VillagePrefix = "Cretteville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Cricqueville
  do
    local VillagePrefix = "Cricqueville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Deux
  do
    local VillagePrefix = "Deux" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Evreux
  do
    local VillagePrefix = "Evreux" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Ford
  do
    local VillagePrefix = "Ford" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Funtington
  do
    local VillagePrefix = "Funtington" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Lantheuil
  do
    local VillagePrefix = "Lantheuil" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Lessay
  do
    local VillagePrefix = "Lessay" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Lignerolles
  do
    local VillagePrefix = "Lignerolles" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Longues
  do
    local VillagePrefix = "Longues" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Maupertus 
  do
    local VillagePrefix = "Maupertus" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Meautis
  do
    local VillagePrefix = "Meautis" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Molay
  do
    local VillagePrefix = "Molay" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Oar
  do
    local VillagePrefix = "Oar" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Picauville
  do
    local VillagePrefix = "Picauville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Rucqueville
  do
    local VillagePrefix = "Rucqueville" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- SaintPierre
  do
    local VillagePrefix = "SaintPierre" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- SainteCroix
  do
    local VillagePrefix = "SainteCroix" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  --SainteLaurent
  do
    local VillagePrefix = "SainteLaurent" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Sommervieu
  do
    local VillagePrefix = "Sommervieu" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  
  -- Tangmere
  do
    local VillagePrefix = "Tangmere" 
    local Runway1 = GROUP:FindByName( VillagePrefix .. " 1" )
    local Zone1 = ZONE_POLYGON:New( VillagePrefix .. " 1", Runway1 ):SmokeZone(SMOKECOLOR.Red):Flush()
    local Runway2 = GROUP:FindByName( VillagePrefix .. " 2" )
    local Zone2 = ZONE_POLYGON:New( VillagePrefix .. " 2", Runway2 ):SmokeZone(SMOKECOLOR.Red):Flush()
  end
  
  --]]
  
  return self
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/master/DET%20-%20Detection)
-- 
-- ===
-- 
-- Facilitate the detection of enemy units within the battle zone executed by FACs (Forward Air Controllers) or RECCEs (Reconnassance 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 @{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 } )
  -- 
  -- 
  -- ## **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 additing 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 behaviour 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.
  -- Expecially 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 were a detection needs to be very realisting 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 rejectance 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>
  
  --- @type DETECTION_BASE.DetectedItem
  -- @field #boolean IsDetected Indicates if the DetectedItem has been detected or not.
  -- @field Core.Set#SET_UNIT Set
  -- @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 changes.
  -- @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.
  
  --- DETECTION constructor.
  -- @param #DETECTION_BASE self
  -- @param Core.Set#SET_GROUP DetectionSetGroup The @{Set} of GROUPs in the Forward Air Controller role.
  -- @return #DETECTION_BASE self
  function DETECTION_BASE:New( DetectionSetGroup )
  
    -- Inherits from BASE
    local self = BASE:Inherit( self, FSM:New() ) -- #DETECTION_BASE
    
    self.DetectedItemCount = 0
    self.DetectedItemMax = 0
    self.DetectedItems = {}
    
    self.DetectionSetGroup = DetectionSetGroup
    
    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", "DetectionGroup", "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.
    	
    --- Synchronous Event Trigger for Event Detected.
    -- @function [parent=#DETECTION_BASE] Detected
    -- @param #DETECTION_BASE self
    
    --- Asynchronous Event Trigger for Event Detected.
    -- @function [parent=#DETECTION_BASE] __Detected
    -- @param #DETECTION_BASE self
    -- @param #number Delay The delay in seconds.
    
    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 #table DetectedItem The DetectedItem.
      
    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.1
      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
      for DetectionGroupID, DetectionGroupData in pairs( self.DetectionSetGroup:GetSet() ) do
        --self:F( { DetectionGroupData } )
        self:F( { DetectionGroup = DetectionGroupData:GetName() } )
        self:__DetectionGroup( DetectDelay, DetectionGroupData, DetectionTimeStamp ) -- Process each detection asynchronously.
        self.DetectionCount = self.DetectionCount + 1
        DetectDelay = DetectDelay + 1
      end
    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 DetectionGroup The Group detecting.
    -- @param #number DetectionTimeStamp Time stamp of detection event.
    function DETECTION_BASE:onafterDetectionGroup( From, Event, To, DetectionGroup, DetectionTimeStamp  )
      
      --self:F( { DetectedObjects = self.DetectedObjects } )
      
      self.DetectionRun = self.DetectionRun + 1
      
      local HasDetectedObjects = false
      
      if DetectionGroup:IsAlive() then
    
        --self:T( { "DetectionGroup is Alive", DetectionGroup:GetName() } )
        
        local DetectionGroupName = DetectionGroup:GetName()
        local DetectionUnit = DetectionGroup:GetUnit(1)
        
        local DetectedUnits = {}
        
        local DetectedTargets = DetectionGroup:GetDetectedTargets(
          self.DetectVisual,
          self.DetectOptical,
          self.DetectRadar,
          self.DetectIRST,
          self.DetectRWR,
          self.DetectDLINK
        )
        
        self:F( { DetectedTargets = DetectedTargets } )
        
        for DetectionObjectID, Detection in pairs( DetectedTargets ) 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 ) do
        
          local DetectedObject = DetectedObjectData.Object
          
          if DetectedObject:isExist() then
  
            local TargetIsDetected, TargetIsVisible, TargetLastTime, TargetKnowType, TargetKnowDistance, 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 = DetectionGroup:GetVec3()
              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:IsPointVec2InZone( DetectedObjectVec2 ) == true then
                    DetectionAccepted = false
                  end
                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:IsPointVec2InZone( 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 + 60 <= 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
 
        self:__Detect( self.RefreshTimeInterval )
      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:GetDetectedSet( 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

  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 IntereptDelay 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 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 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
    -- @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
    -- @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 deteted item ...
    -- @param #DETECTION_BASE self
    -- @return #number The distance. 
    function DETECTION_BASE:GetFriendliesDistance( DetectedItem )
      
      return DetectedItem.FriendliesDistance
    end
  
    --- Returns if there are friendlies nearby the FAC units ...
    -- @param #DETECTION_BASE self
    -- @return #boolean trhe 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
    -- @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
    function DETECTION_BASE:ReportFriendliesNearBy( TargetData )
      --self:F( { "Search Friendlies", DetectedItem = TargetData.DetectedItem } )
      
      local DetectedItem = TargetData.DetectedItem  -- Functional.Detection#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
        local DetectionZone = ZONE_UNIT:New( "DetectionPlayers", DetectedUnit, self.FriendliesRange )
        
        _DATABASE:ForEachPlayer(
          --- @param Wrapper.Unit#UNIT PlayerUnit
          function( PlayerUnitName )
            local PlayerUnit = UNIT:FindByName( PlayerUnitName )

            if PlayerUnit and PlayerUnit:IsInZone(DetectionZone) 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    

      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 ItemPrefix
  -- @param DetectedItemKey The key of the DetectedItem.
  -- @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 = {}
    self.DetectedItemCount = self.DetectedItemCount + 1
    self.DetectedItemMax = self.DetectedItemMax + 1
    
    if DetectedItemKey then
      self.DetectedItems[DetectedItemKey] = DetectedItem
    else
      self.DetectedItems[self.DetectedItemMax] = DetectedItem
    end
    
    self.DetectedItemsByIndex[self.DetectedItemMax] = DetectedItem
    
    
    DetectedItem.Set = Set or SET_UNIT:New():FilterDeads():FilterCrashes()
    DetectedItem.Index = DetectedItemKey or self.DetectedItemMax
    DetectedItem.ItemID = ItemPrefix .. "." .. self.DetectedItemMax
    DetectedItem.ID = self.DetectedItemMax
    DetectedItem.Removed = false
    
    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( DetectedItemKey, Set, Zone )
  
    local DetectedItem = self:AddDetectedItem( "AREA", 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 doen 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( { DetectedItemsByIndex = 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 detecttion area using a given numeric index.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem DetectedItem
  -- @return Core.Set#SET_UNIT DetectedSet
  function DETECTION_BASE:GetDetectedSet( 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
  -- @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  


  --- 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

  --- Set the detected item threatlevel.
  -- @param #DETECTION_BASE self
  -- @param #DETECTION_BASE.DetectedItem The DetectedItem to calculate the threatlevel 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 detectedion 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 Groups.
  -- @param #DETECTION_BASE self
  -- @return Core.Set#SET_GROUP
  function DETECTION_BASE:GetDetectionSetGroup()
  
    local DetectionSetGroup = self.DetectionSetGroup
    return DetectionSetGroup
  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.DetectionSetGroup: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 @{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 @{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 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.DetectionSetGroup } ) -- 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.
  -- @return Core.Report#REPORT The report of the detection items.
  function DETECTION_UNITS:DetectedItemReportSummary( DetectedItem, AttackGroup, Settings )
    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 )
 
      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 @{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 @{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
          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.DetectionSetGroup } ) -- 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:GetDetectedSet( 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, @{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) is 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 @{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_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 @{Set} of GROUPs in the Forward Air Controller role.
  -- @param DCS#Distance DetectionZoneRange The range till which targets are grouped upon the first detected target.
  -- @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
    
    self._SmokeDetectedUnits = false
    self._FlareDetectedUnits = false
    self._SmokeDetectedZones = false
    self._FlareDetectedZones = false
    self._BoundDetectedZones = false
    
    return self
  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:GetDetectedSet( 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_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 clsses.
  -- @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 recentered 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( 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.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_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** -- 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.
--  
-- ===
-- 
-- ## Missions:
-- 
-- [DES - Designation](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/DES%20-%20Designation)
-- 
-- ===
--
-- 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 @{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**](https://forums.eagle.ru/member.php?u=112175): Showing the way how to lase targets + how laser codes work!!! Explained the autolase script.
--   * [**EasyEB**](https://forums.eagle.ru/member.php?u=112055): Ideas and Beta Testing
--   * [**Wingthor**](https://forums.eagle.ru/member.php?u=123698): 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 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 @{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:GetDetectionSetGroup()
    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.
  -- @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.
  -- @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 ):Text( ", " )
              DetectedReport:Add( string.rep( "-", 140 ) )
              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 )
        
        MenuText = string.format( "(%3s) %s", Designating, MenuText )
        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:GetDetectedSet( 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 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 } )
    
                    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
    
                          function Spot:OnAfterDestroyed( From, Event, To )
                            self.Recce:MessageToSetGroup( "Target " .. TargetUnit:GetTypeName() .. " destroyed. " .. TargetSetUnit:Count() .. " 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 " .. TargetUnit:GetTypeName() .. " with laser " .. RecceUnit:GetSpot().LaserCode .. " for " .. Duration .. "s.", 
                          --                             5, 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!", 5, 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 .. ".", 5, 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:GetDetectedSet( 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:GetDetectedSet( 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 )

          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:GetDetectedSet( 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 )

    self.Designating[Index] = string.gsub( self.Designating[Index], "S", "" )
    self:SetDesignateMenu()
  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 airtraffic 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.
-- 
-- ===
-- 
-- ## Missions:
--
-- ### [RAT - Random Air Traffic](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/RAT%20-%20Random%20Air%20Traffic)
-- 
-- ===
-- 
-- # YouTube Channel
-- 
-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) 
-- ### [MOOSE - RAT - Random Air Traffic](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0u4Zxywtg-mx_ov4vi68CO)
-- 
-- ===
-- 
-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)**
-- 
-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536)
-- 
-- ===
-- @module Functional.Rat
-- @image RAT.JPG

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- RAT class
-- @type RAT
-- @field #string ClassName Name of the Class.
-- @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 coalitons 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.
-- @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 defined in the mission editor 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 theholding point is ~1200 m AGL. Holding patterns might or might not happen with variable duration.
-- * If an aircraft is spawned in air, the procedure omitts 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 beeing 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 consits 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 
-- airstripes. 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 immidiately 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.HelicopterUnsable: 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 beeing despawned immidiately.
-- 
-- 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 availabe 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 indefinetly.
-- 
-- 
-- ### 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 jamms 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 enought 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 coalitons 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.
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Categories of the RAT class. 
-- @list cat
-- @field #string plane Plane.
-- @field #string heli Heli.
RAT.cat={
  plane="plane",
  heli="heli",
}

--- RAT waypoint type.
-- @list 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.
-- @list 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",
}

--- RAT friendly coalitions.
-- @list coal
RAT.coal={
  same="same",
  sameonly="sameonly",
  neutral="neutral",
}

--- RAT unit conversions.
-- @list unit
RAT.unit={
  ft2meter=0.305,
  kmh2ms=0.278,
  FL2m=30.48,
  nm2km=1.852,
  nm2m=1852,
}

--- RAT rules of engagement.
-- @list ROE
RAT.ROE={
  weaponhold="hold",
  weaponfree="free",
  returnfire="return",
}

--- RAT reaction to threat.
-- @list ROT
RAT.ROT={
  evade="evade",
  passive="passive",
  noreaction="noreaction",
}

--- RAT ATC.
-- @list ATC
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 = "2.3.4",
  print = true,
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--TODO list:
--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)
  BASE:F({groupname=groupname, alias=alias})

  -- Inherit SPAWN class.
  self=BASE:Inherit(self, SPAWN:NewWithAlias(groupname, alias)) -- #RAT

  -- Version info.
  if RAT.version.print then
    env.info(RAT.id.."Version "..RAT.version.version)
    RAT.version.print=false
  end

  -- Welcome message.
  self:F(RAT.id..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(RAT.id..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

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- 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 coaliton.
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
    self:_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 beloning to friendly coalition(s).
  self:_GetAirportsOfCoalition()
  
  -- Set submenuname 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
  
  -- 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(RAT.id..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.
  SCHEDULER:New(nil, self.Status, {self}, Tstart+1, self.statusinterval)
  
  -- 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.
  SCHEDULER:New(nil, self._SpawnWithRoute, {self}, Tstart, dt, 0.0, Tstop)
  
  -- Start scheduled activation of uncontrolled groups.
  if self.uncontrolled and self.activate_uncontrolled then
    SCHEDULER:New(nil, self._ActivateUncontrolled, {self}, self.activate_delay, self.activate_delta, self.activate_frand)
  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(RAT.id..string.format("ERROR: 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(RAT.id.."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(RAT.id.."ERROR: 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(RAT.id.."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(RAT.id.."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

--- 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 defined in the mission editor.
-- @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, which has to be defined in the mission editor, 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(RAT.id.."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(RAT.id.."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 oject 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(RAT.id.."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(RAT.id.."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 departure 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 destination 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

--- 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 180 seconds. Minimum is 1.0 seconds.
-- @return #RAT RAT self object.
function RAT:RespawnAfterLanding(delay)
  self:F2(delay)
  delay = delay or 180
  self.respawn_at_landing=true
  delay=math.max(1.0, delay)
  self.respawn_delay=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 accitentally 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

--- Aircraft will be respawned directly after take-off.
-- @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 immediatly.
-- @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

--- Put parking spot coordinates in a data base for future use of aircraft. (Obsolete! API function will be removed soon.)
-- @param #RAT self
-- @param #boolean switch If true, parking spots are memorized. This is also the default setting.
-- @return #RAT RAT self object.
function RAT:ParkingSpotDB(switch)
  self:E("RAT ParkingSpotDB function is obsolete and will be removed soon!")
  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

--- 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

--- Activate uncontrolled aircraft. 
-- @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 ~500 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(500, 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(RAT.id.."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
  self.aircraft.length=DCSdesc.box.max.x
  self.aircraft.height=DCSdesc.box.max.y
  self.aircraft.width=DCSdesc.box.max.z
  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(RAT.id..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=RAT.id, departure=_departure, destination=_destination, takeoff=_takeoff, landing=_landing, livery=_livery, waypoint=_waypoint, lastpos=_lastpos, nrespawn=_nrespawn})

  -- 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, WPholding, WPfinal = 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(RAT.id..text)
  else
    livery=nil
  end
  
  -- Modify the spawn template to follow the flight plan.
  local successful=self:_ModifySpawnTemplate(waypoints, livery, _lastpos, departure, takeoff, parkingdata)
  if not successful then
    return nil
  end
  
  -- Actually spawn the group.
  local group=self:SpawnWithIndex(self.SpawnIndex) -- Wrapper.Group#GROUP
  
  -- Increase counter of alive groups (also uncontrolled ones).
  self.alive=self.alive+1
  self:T(RAT.id..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
    if self.returnzone then
      self:_ATCAddFlight(group:GetName(), departure:GetName())
    else
      self:_ATCAddFlight(group:GetName(), destination:GetName())
    end
  end
  
  -- Place markers of waypoints on F10 map.
  if self.placemarkers then
    self:_PlaceMarkers(waypoints, self.SpawnIndex)
  end
  
  -- Set group to be invisible.
  if self.invisible then
    self:_CommandInvisible(group, true)
  end
  
  -- Set group to be immortal.
  if self.immortal then
    self:_CommandImmortal(group, true)
  end
  
  -- Set ROE, default is "weapon hold".
  self:_SetROE(group, self.roe)
  
  -- Set ROT, default is "no reaction".
  self:_SetROT(group, self.rot)

  -- Init ratcraft array.  
  self.ratcraft[self.SpawnIndex]={}
  self.ratcraft[self.SpawnIndex]["group"]=group
  self.ratcraft[self.SpawnIndex]["destination"]=destination
  self.ratcraft[self.SpawnIndex]["departure"]=departure
  self.ratcraft[self.SpawnIndex]["waypoints"]=waypoints
  self.ratcraft[self.SpawnIndex]["airborne"]=group:InAir()
  self.ratcraft[self.SpawnIndex]["nunits"]=group:GetInitialSize()
  -- Time and position on ground. For check if aircraft is stuck somewhere.
  if group:InAir() then
    self.ratcraft[self.SpawnIndex]["Tground"]=nil
    self.ratcraft[self.SpawnIndex]["Pground"]=nil
    self.ratcraft[self.SpawnIndex]["Uground"]=nil
    self.ratcraft[self.SpawnIndex]["Tlastcheck"]=nil
  else
    self.ratcraft[self.SpawnIndex]["Tground"]=timer.getTime()
    self.ratcraft[self.SpawnIndex]["Pground"]=group:GetCoordinate()
    self.ratcraft[self.SpawnIndex]["Uground"]={}
    for _,_unit in pairs(group:GetUnits()) do
      local _unitname=_unit:GetName()
      self.ratcraft[self.SpawnIndex]["Uground"][_unitname]=_unit:GetCoordinate()
    end
    self.ratcraft[self.SpawnIndex]["Tlastcheck"]=timer.getTime()
  end
  -- Initial and current position. For calculating the travelled distance.
  self.ratcraft[self.SpawnIndex]["P0"]=group:GetCoordinate()
  self.ratcraft[self.SpawnIndex]["Pnow"]=group:GetCoordinate()
  self.ratcraft[self.SpawnIndex]["Distance"]=0
  
  -- Each aircraft gets its own takeoff type.
  self.ratcraft[self.SpawnIndex].takeoff=takeoff
  self.ratcraft[self.SpawnIndex].landing=landing
  self.ratcraft[self.SpawnIndex].wpholding=WPholding
  self.ratcraft[self.SpawnIndex].wpfinal=WPfinal
  
  -- Aircraft is active or spawned in uncontrolled state.
  self.ratcraft[self.SpawnIndex].active=not self.uncontrolled
  
  -- Set status to spawned. This will be overwritten in birth event.
  self.ratcraft[self.SpawnIndex]["status"]=RAT.status.Spawned
  
  -- Livery
  self.ratcraft[self.SpawnIndex].livery=livery
  
  -- If this switch is set to true, the aircraft will be despawned the next time the status function is called.
  self.ratcraft[self.SpawnIndex].despawnme=false
  
  -- Number of preformed spawn attempts for this group.
  self.ratcraft[self.SpawnIndex].nrespawn=nrespawn
 
  -- 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, group, RAT.ROE.weaponhold)
    MENU_MISSION_COMMAND:New("Weapons free", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, RAT.ROE.weaponfree)
    MENU_MISSION_COMMAND:New("Return fire",  self.Menu[self.SubMenuName].groups[self.SpawnIndex]["roe"], self._SetROE, self, group, 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, group, RAT.ROT.noreaction)
    MENU_MISSION_COMMAND:New("Passive defense", self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, RAT.ROT.passive)
    MENU_MISSION_COMMAND:New("Evade on fire",   self.Menu[self.SubMenuName].groups[self.SpawnIndex]["rot"], self._SetROT, self, group, 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
  
  return self.SpawnIndex
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(RAT.id.."ATC: User flag value (landing) for "..name.." set to "..flagvalue)
end

--- Respawn a group.
-- @param #RAT self
-- @param #number index Spawn index.
-- @param Core.Point#COORDINATE lastpos Last known position of the group.
-- @param #number delay Delay before respawn
function RAT:_Respawn(index, lastpos, delay)
  
  -- Get the spawn index from group
  --local index=self:GetSpawnIndexFromGroup(group)
  
  -- Get departure and destination from previous journey.
  local departure=self.ratcraft[index].departure
  local destination=self.ratcraft[index].destination
  local takeoff=self.ratcraft[index].takeoff
  local landing=self.ratcraft[index].landing
  local livery=self.ratcraft[index].livery
  local lastwp=self.ratcraft[index].waypoints[#self.ratcraft[index].waypoints]
  --local lastpos=group:GetCoordinate()
  
  local _departure=nil
  local _destination=nil
  local _takeoff=nil
  local _landing=nil
  local _livery=nil
  local _lastwp=nil
  local _lastpos=nil
 
  if self.continuejourney then
  
    -- 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.
    -- TODO: Need to think if continuejourney 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 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
  
    -- 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
  if delay then
    respawndelay=delay
  elseif self.respawn_delay then
    respawndelay=self.respawn_delay+3  -- despawn happens after self.respawndelay. We add another 3 sec for free parking.
  else
    respawndelay=3
  end
      
  -- Spawn new group.
  local arg={}
  arg.self=self
  arg.departure=_departure
  arg.destination=_destination
  arg.takeoff=_takeoff
  arg.landing=_landing
  arg.livery=_livery
  arg.lastwp=_lastwp
  arg.lastpos=_lastpos
  self:T(RAT.id..string.format("%s delayed respawn in %.1f seconds.", self.alias, respawndelay))
  SCHEDULER:New(nil, self._SpawnWithRouteTimer, {arg}, respawndelay)
  
end

--- Delayed spawn function called by scheduler.
-- @param #RAT self
-- @param #table arg Parameters: arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos
function RAT._SpawnWithRouteTimer(arg)
  RAT._SpawnWithRoute(arg.self, arg.departure, arg.destination, arg.takeoff, arg.landing, arg.livery, arg.lastwp, arg.lastpos)
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.Airport#AIRBASE _departure (Optional) Departure airbase.
-- @param Wrapper.Airport#AIRBASE _destination (Optional) Destination airbase.
-- @param #table _waypoint Initial waypoint.
-- @return Wrapper.Airport#AIRBASE Departure airbase.
-- @return Wrapper.Airport#AIRBASE Destination airbase.
-- @return #table Table of flight plan waypoints.
-- @return #nil If no valid departure or destination airport could be found.
function RAT:_SetRoute(takeoff, landing, _departure, _destination, _waypoint)
    
  -- Max cruise speed.
  local VxCruiseMax
  if self.Vcruisemax then
    -- User input.
    VxCruiseMax = 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
  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:New(_departure)
    else
      local text=string.format("ERROR! Specified departure airport %s does not exist for %s.", _departure, self.alias)
      self:E(RAT.id..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(RAT.id..text)
    return nil
  end

  -- Coordinates of departure point.
  local Pdeparture
  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(RAT.id..text)
  end
  
  -- DESTINATION AIRPORT
  local destination=nil
  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:New(_destination)
    else
      local text=string.format("ERROR: Specified destination airport/zone %s does not exist for %s!", _destination, self.alias)
      self:E(RAT.id.."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(RAT.id.."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(RAT.id.."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
  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(RAT.id..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 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)
  self.waypointdescriptions[#wp]="Departure"
  self.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)
      self.waypointdescriptions[#wp]="Begin of Cruise"
      self.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)
    self.waypointdescriptions[#wp]="Climb"
    self.waypointstatus[#wp]=RAT.status.Climb
    
    wp[#wp+1]=self:_Waypoint(#wp+1, "Begin of Cruise", RAT.wp.cruise, c[#wp+1], VxCruise, FLcruise)
    self.waypointdescriptions[#wp]="Begin of Cruise"
    self.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)
    self.waypointdescriptions[#wp]="Return Zone"
    self.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)
    self.waypointdescriptions[#wp]="Final Destination"
    self.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)
    self.waypointdescriptions[#wp]="End of Cruise"
    self.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)
    self.waypointdescriptions[#wp]="End of Cruise"
    self.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)
      self.waypointdescriptions[#wp]="Descent"
      self.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)
      self.waypointdescriptions[#wp]="Descent"
      self.waypointstatus[#wp]=RAT.status.DescentHolding
    end
  end
  
  -- Holding and final destination.
  if landing==RAT.wp.landing then

    -- Holding point
    c[#c+1]=Pholding  
    wp[#wp+1]=self:_Waypoint(#wp+1, "Holding Point", RAT.wp.holding, c[#wp+1], VxHolding, H_holding+h_holding)
    self.waypointdescriptions[#wp]="Holding Point"
    self.waypointstatus[#wp]=RAT.status.Holding
    wpholding=#wp

    -- Final destination.
    c[#c+1]=Pdestination    
    wp[#wp+1]=self:_Waypoint(#wp+1, "Final Destination", landing, c[#wp+1], VxFinal, H_destination, destination)
    self.waypointdescriptions[#wp]="Final Destination"
    self.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:")
  
  -- 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, wpholding, wpfinal    
  else
    return departure, destination, waypoints, wpholding, wpfinal
  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:New(name)
        else
          self:E(RAT.id..string.format("ERROR! Takeoff is not in air. Cannot use %s as departure.", name))
        end
      else
        self:E(RAT.id..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(RAT.id..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(RAT.id..text)
  else
    self:E(RAT.id..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:New(name)
          else
            self:E(RAT.id..string.format("ERROR! Landing is not in air. Cannot use zone %s as destination!", name))
          end
        else
          self:E(RAT.id..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(RAT.id..text)
          end
        end
        
      end
    end 
  end
  
  -- Info message.
  self:T(RAT.id..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(RAT.id..text)
    --MESSAGE:New(text, 30):ToAllIf(self.Debug)
    
  else
    self:E(RAT.id.."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)
      
      -- 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(RAT.id..text)
    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:GetDesc().category
      if airport:GetCoalition()==coalition then
        -- Planes cannot land on FARPs.
        --local condition1=self.category==RAT.cat.plane and airport:GetTypeName()=="FARP"
        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 airport:GetCategory()==1
        local condition2=self.category==RAT.cat.plane and category==Airbase.Category.SHIP
        
        -- Check that airport has the requested terminal types.
        -- NOT good here because we would also not allow any airport zones!
        --[[
        local nspots=1
        if self.termtype then
          nspots=airport:GetParkingSpotsNumber(self.termtype)
        end
        local condition3 = nspots==0
        ]]
        
        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(RAT.id..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)

  -- Optional arguments.
  if message==nil then
    message=false
  end  
  if forID==nil then
    forID=false
  end
  
  -- Current time.
  local Tnow=timer.getTime()
  
  -- Alive counter.
  local nalive=0
    
  -- Loop over all ratcraft.
  for spawnindex,ratcraft in ipairs(self.ratcraft) do
  
    -- Get group.
    local group=ratcraft.group  --Wrapper.Group#GROUP
    
    if group and group:IsAlive() then
      nalive=nalive+1
            
      -- 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.y
      --local vel=group:GetVelocityKMH()
      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 -- group:GetSize()
      local N0units=group:GetInitialSize()
             
      -- Monitor time and distance on ground.
      local Tg=0
      local Dg=0
      local dTlast=0
      local stationary=false --lets assume, we did move
      if airborne then
        -- Aircraft is airborne.
        ratcraft["Tground"]=nil
        ratcraft["Pground"]=nil
        ratcraft["Uground"]=nil
        ratcraft["Tlastcheck"]=nil
      else
        --Aircraft is on ground.
        if ratcraft["Tground"] then
          -- Aircraft was already on ground. Calculate total time on ground.
          Tg=Tnow-ratcraft["Tground"]
          
          -- Distance on ground since last check.
          Dg=coords:Get2DDistance(ratcraft["Pground"])
          
          -- Time interval since last check.
          dTlast=Tnow-ratcraft["Tlastcheck"]
          
          -- If more than Tinactive seconds passed since last check ==> check how much we moved meanwhile.
          if dTlast > self.Tinactive then
                  
            --[[
            if Dg<50 and active and status~=RAT.status.EventBirth then
              stationary=true
            end
            ]]
            
            -- Loop over all units.
            for _,_unit in pairs(group:GetUnits()) do
            
              if _unit and _unit:IsAlive() then
              
                -- Unit name, coord and distance since last check.
                local unitname=_unit:GetName()
                local unitcoord=_unit:GetCoordinate()
                local Ug=unitcoord:Get2DDistance(ratcraft.Uground[unitname])
                
                -- Debug info
                self:T2(RAT.id..string.format("Unit %s travelled distance on ground %.1f m since %d seconds.", unitname, Ug, dTlast))
                
                -- If aircraft did not move more than 50 m since last check, we call it stationary and despawn it.
                -- Aircraft which are spawned uncontrolled or starting their engines are not counted. 
                if Ug<50 and active and status~=RAT.status.EventBirth then
                  stationary=true
                end
                
                -- Update coords.
                ratcraft["Uground"][unitname]=unitcoord
              end
            end
            
            -- Set the current time to know when the next check is necessary.
            ratcraft["Tlastcheck"]=Tnow
            ratcraft["Pground"]=coords            
          end
          
        else
          -- First time we see that the aircraft is on ground. Initialize the times and position.
          ratcraft["Tground"]=Tnow
          ratcraft["Tlastcheck"]=Tnow
          ratcraft["Pground"]=coords
          ratcraft["Uground"]={}
          for _,_unit in pairs(group:GetUnits()) do
            local unitname=_unit:GetName()
            ratcraft.Uground[unitname]=_unit:GetCoordinate()
          end
        end
      end
      
      -- Monitor travelled distance since last check.
      local Pn=coords
      local Dtravel=Pn:Get2DDistance(ratcraft["Pnow"])
      ratcraft["Pnow"]=Pn
      
      -- Add up the travelled distance.
      ratcraft["Distance"]=ratcraft["Distance"]+Dtravel
      
      -- Distance remaining to destination.
      local Ddestination=Pn:Get2DDistance(ratcraft.destination:GetCoordinate())
   
      -- 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("Speed = %i km/h\n", vel)
        text=text..string.format("Distance travelled        = %6.1f km\n", ratcraft["Distance"]/1000)
        text=text..string.format("Distance to destination = %6.1f km", Ddestination/1000)
        if not airborne then
          text=text..string.format("\nTime on ground  = %6.0f seconds\n", Tg)
          text=text..string.format("Position change = %8.1f m since %3.0f seconds.", Dg, dTlast)
        end
        self:T(RAT.id..text)
        if message then
          MESSAGE:New(text, 20):ToAll()
        end
      end
      
      -- Despawn groups if they are on ground and don't move or are damaged.
      if not airborne then
      
        -- Despawn unit if it did not move more then 50 m in the last 180 seconds.
        if stationary then
          local text=string.format("Group %s is despawned after being %d seconds inaktive on ground.", self.alias, dTlast)
          self:T(RAT.id..text)
          self:_Despawn(group)
        end
        
        -- Despawn group if life is < 10% and distance travelled < 100 m.
        if life<10 and Dtravel<100 then
          local text=string.format("Damaged group %s is despawned. Life = %3.0f", self.alias, life)
          self:T(RAT.id..text)
          self:_Despawn(group)
        end
        
      end
      
      -- Despawn groups after they have reached their destination zones.
      if ratcraft.despawnme then
      
        local text=string.format("Flight %s will be despawned NOW!", self.alias)
        self:T(RAT.id..text)
        -- Despawn old group.
        if (not self.norespawn) and (not self.respawn_after_takeoff) then
          local idx=self:GetSpawnIndexFromGroup(group)
          local coord=group:GetCoordinate()  
          self:_Respawn(idx, coord, 0)
        end
        self:_Despawn(group, 0)
        
      end

    else
      -- Group does not exist.
      local text=string.format("Group does not exist in loop ratcraft status.")
      self:T2(RAT.id..text)
    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(RAT.id..text)
  MESSAGE:New(text, 20):ToAllIf(message and not forID)
  
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(RAT.id.."ERROR! Unit does not exist in RAT_Getlife(). Returning zero.")
    end
  else
    self:T2(RAT.id.."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 index=self:GetSpawnIndexFromGroup(group)
    
    if self.ratcraft[index] then
    
      -- Set new status.
      self.ratcraft[index].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(RAT.id..text)
      
      if not (no1 or no2 or no3) then
        MESSAGE:New(text, 10):ToAllIf(self.reportstatus)
      end
      
    end
    
  end
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 index=self:GetSpawnIndexFromGroup(group)
    
    if self.ratcraft[index] then
    
      -- Set new status.
      return self.ratcraft[index].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(RAT.id.."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)
    
    if EventPrefix then
    
      -- Check that the template name actually belongs to this object.
      if EventPrefix == self.alias then
    
        local text="Event: Group "..SpawnGroup:GetName().." was born."
        self:T(RAT.id..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 _departure=self.ratcraft[i].departure:GetName()
        local _destination=self.ratcraft[i].destination:GetName()
        local _nrespawn=self.ratcraft[i].nrespawn
        local _takeoff=self.ratcraft[i].takeoff
        local _landing=self.ratcraft[i].landing
        local _livery=self.ratcraft[i].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(RAT.id..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(RAT.id..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(RAT.id..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:GetDesc().category==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(RAT.id..text)
          if self.Debug then
            SpawnGroup:FlareYellow()
          end
          -- Despawn group.
          self:_Despawn(SpawnGroup)
        end
        
      end
    end
  else
    self:T2(RAT.id.."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(RAT.id.."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)
    
    if EventPrefix then
    
      -- Check that the template name actually belongs to this object.
      if EventPrefix == self.alias then
  
        local text="Event: Group "..SpawnGroup:GetName().." started engines."
        self:T(RAT.id..text)
    
        -- Set status.
        local status
        if SpawnGroup:InAir() then
          status=RAT.status.EventEngineStartAir
        else
          status=RAT.status.EventEngineStart
        end
        self:_SetStatus(SpawnGroup, status)
      end
    end
    
  else
    self:T2(RAT.id.."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)
    
    if EventPrefix then
    
      -- Check that the template name actually belongs to this object.
      if EventPrefix == self.alias then
  
        local text="Event: Group "..SpawnGroup:GetName().." is airborne."
        self:T(RAT.id..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(RAT.id..text)
        
          -- Respawn group. We respawn with no parameters from the old flight.
          self:_SpawnWithRoute(nil, nil, nil, nil, nil, nil, nil, nil)
        end
        
      end
    end
    
  else
    self:T2(RAT.id.."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)
    
    if EventPrefix then
    
      -- Check that the template name actually belongs to this object.
      if EventPrefix == self.alias then
  
        local text="Event: Group "..SpawnGroup:GetName().." landed."
        self:T(RAT.id..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(RAT.id..text)
        
          -- Respawn group.
          local idx=self:GetSpawnIndexFromGroup(SpawnGroup)
          local coord=SpawnGroup:GetCoordinate()
          self:_Respawn(idx, coord)
        end
        
      end
    end
    
  else
    self:T2(RAT.id.."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(RAT.id.."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(RAT.id..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(RAT.id..text)
          
            -- Respawn group.
            local idx=self:GetSpawnIndexFromGroup(SpawnGroup)
            local coord=SpawnGroup:GetCoordinate()
            self:_Respawn(idx, coord)
          end

          -- Despawn group.
          text="Event: Group "..SpawnGroup:GetName().." will be destroyed now."
          self:T(RAT.id..text)
          self:_Despawn(SpawnGroup)                   

        end

      end
    end
    
  else
    self:T2(RAT.id.."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(RAT.id..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(RAT.id..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(RAT.id.."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(RAT.id..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(RAT.id.."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(RAT.id..text)
    
        -- Set status.
        local status=RAT.status.EventDead
        self:_SetStatus(SpawnGroup, status)
        
      end
    end

  else
    self:T2(RAT.id.."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(RAT.id.."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
    
      -- Update number of alive units in the group.
      local _i=self:GetSpawnIndexFromGroup(SpawnGroup)
      self.ratcraft[_i].nunits=self.ratcraft[_i].nunits-1
      local _n=self.ratcraft[_i].nunits
      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, _n, _n0)
      self:T(RAT.id..text)
     
      -- Set status.
      local status=RAT.status.EventCrash
      self:_SetStatus(SpawnGroup, status)
      
      -- Respawn group if all units are dead.
      if _n==0 and self.respawn_after_crash and not self.norespawn then
        local text=string.format("No units left of group %s. Group will be respawned now.", SpawnGroup:GetName())
        self:T(RAT.id..text)
        -- Respawn group.
        local idx=self:GetSpawnIndexFromGroup(SpawnGroup)
        local coord=SpawnGroup:GetCoordinate()
        self:_Respawn(idx, coord)
      end

    end
    
  else
    if self.Debug then
      self:E(RAT.id.."ERROR: Group does not exist in RAT:_OnCrash().")
    end
  end
end

--- Despawn unit. Unit gets destoyed and group is set to nil.
-- Index of ratcraft array is taken from spawned group name.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group to be despawned.
-- @param #number delay Delay in seconds before the despawn happens.
function RAT:_Despawn(group, delay)

  if group ~= nil then
  
    -- Get spawnindex of group.
    local index=self:GetSpawnIndexFromGroup(group)
    
    if index ~= nil then
    
      self.ratcraft[index].group=nil
      self.ratcraft[index]["status"]="Dead"
      
      --TODO: Maybe here could be some more arrays deleted?      
      --TODO: Somehow this causes issues.
      --[[
      --self.ratcraft[index]["group"]=group
      self.ratcraft[index]["destination"]=nil
      self.ratcraft[index]["departure"]=nil
      self.ratcraft[index]["waypoints"]=nil
      self.ratcraft[index]["airborne"]=nil
      self.ratcraft[index]["Tground"]=nil
      self.ratcraft[index]["Pground"]=nil
      self.ratcraft[index]["Tlastcheck"]=nil
      self.ratcraft[index]["P0"]=nil
      self.ratcraft[index]["Pnow"]=nil
      self.ratcraft[index]["Distance"]=nil
      self.ratcraft[index].takeoff=nil
      self.ratcraft[index].landing=nil
      self.ratcraft[index].wpholding=nil
      self.ratcraft[index].wpfinal=nil
      self.ratcraft[index].active=false
      self.ratcraft[index]["status"]=nil
      self.ratcraft[index].livery=nil
      self.ratcraft[index].despawnme=nil
      self.ratcraft[index].nrespawn=nil
      ]]
      -- Remove ratcraft table entry.
      --table.remove(self.ratcraft, index)
      
      
       -- 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 despawndelay=0
      if delay then
        -- Explicitly requested delay time.
        despawndelay=delay
      elseif self.respawn_delay then
        -- Despawn afer respawn_delay. Actual respawn happens in +3 seconds to allow for free parking.
        despawndelay=self.respawn_delay
      end
      
      -- This will destroy the DCS group and create a single DEAD event.      
      --if despawndelay>0.5 then
        self:T(RAT.id..string.format("%s delayed despawn in %.1f seconds.", self.alias, despawndelay))
        SCHEDULER:New(nil, self._Destroy, {self, group}, despawndelay)
      --else
        --self:_Destroy(group)
      --end  

      -- Remove submenu for this group.
      if self.f10menu and self.SubMenuName ~= nil then
        self.Menu[self.SubMenuName]["groups"][index]:Remove()
      end
      
    end
  end
end

--- Destroys the RAT DCS group and all of its DCS units.
-- Note that this raises a DEAD event at run-time.
-- So all event listeners will catch the DEAD event of this DCS group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group The RAT group to be destroyed.
function RAT:_Destroy(group)
  self:F2(group)

  local DCSGroup = group:GetDCSObject() -- DCS#Group

  if DCSGroup and DCSGroup:isExist() then
    
    -- Cread one single Dead event and delete units from database.
    local triggerdead=true
    for _,DCSUnit in pairs(DCSGroup:getUnits()) do
    
      -- Dead event.
      if DCSUnit then
        if triggerdead then
          self:_CreateEventDead(timer.getTime(), DCSUnit)
          triggerdead=false
        end
      
        -- Delete from data base.
        _DATABASE:DeleteUnit(DCSUnit:getName())
      end
    end
    
    -- Destroy DCS group.
    DCSGroup:destroy()
    DCSGroup = nil
  end

  return nil
end

--- Create a Dead event.
-- @param #RAT self
-- @param DCS#Time EventTime The time stamp of the event.
-- @param DCS#Object Initiator The initiating object of the event.
function RAT:_CreateEventDead(EventTime, Initiator)
  self:F( { EventTime, Initiator } )

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

  world.onEvent( Event )
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(RAT.id.."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(RAT.id..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:GetDesc().category
    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(RAT.id.."Unknown Airport category in _Waypoint()!")
    end  
  end
  -- properties
  RoutePoint.properties = {
    ["vnav"]   = 1,
    ["scale"]  = 0,
    ["angle"]  = 0,
    ["vangle"] = 0,
    ["steer"]  = 2,
  }
  -- tasks
  local TaskCombo = {}
  local TaskHolding  = self:_TaskHolding({x=Coord.x, y=Coord.z}, Altitude, Speed, self:_Randomize(90,0.9))
  local TaskWaypoint = self:_TaskFunction("RAT._WaypointFunction", self, index)
  
  RoutePoint.task = {}
  RoutePoint.task.id = "ComboTask"
  RoutePoint.task.params = {}
  
  TaskCombo[#TaskCombo+1]=TaskWaypoint
  if Type==RAT.wp.holding then
    TaskCombo[#TaskCombo+1]=TaskHolding
  end
  
  RoutePoint.task.params.tasks = TaskCombo

  -- 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.
-- @return #number total Total route length in meters.
function RAT:_Routeinfo(waypoints, comment)
  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, self.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, self.waypointdescriptions[i], self.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(RAT.id..text)
  
  -- return total route length in meters
  return total
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Orbit at a specified position at a specified alititude with a specified speed.
-- @param #RAT self
-- @param DCS#Vec2 P1 The point to hold the position.
-- @param #number Altitude The altitude ASL at which to hold the position.
-- @param #number Speed The speed flying when holding the position in m/s.
-- @param #number Duration Duration of holding pattern in seconds.
-- @return DCS#Task DCSTask
function RAT:_TaskHolding(P1, Altitude, Speed, Duration)

  --local LandHeight = land.getHeight(P1)

  --TODO: randomize P1
  -- Second point is 3 km north of P1 and 200 m for helos.
  local dx=3000
  local dy=0
  if self.category==RAT.cat.heli then
    dx=200
    dy=0
  end
  
  local P2={}
  P2.x=P1.x+dx
  P2.y=P1.y+dy
  local Task = {
    id = 'Orbit',
    params = {
      pattern = AI.Task.OrbitPattern.RACE_TRACK,
      --pattern = AI.Task.OrbitPattern.CIRCLE,
      point = P1,
      point2 = P2,
      speed = Speed,
      altitude = Altitude
    }
  }
  
  local DCSTask={}
  DCSTask.id="ControlledTask"
  DCSTask.params={}
  DCSTask.params.task=Task
  
  if self.ATCswitch then
    -- Set stop condition for holding. Either flag=1 or after max. X min holding.
    local userflagname=string.format("%s#%03d", self.alias, self.SpawnIndex+1)
    local maxholdingduration=60*120
    DCSTask.params.stopCondition={userFlag=userflagname, userFlagValue=1, duration=maxholdingduration}
  else
    DCSTask.params.stopCondition={duration=Duration}
  end
  
  return DCSTask
end

--- Function which is called after passing every waypoint. Info on waypoint is given and special functions are executed.
-- @param Core.Group#GROUP group Group of aircraft.
-- @param #RAT rat RAT object.
-- @param #number wp Waypoint index. Running number of the waypoints. Determines the actions to be executed.
function RAT._WaypointFunction(group, rat, wp)

  -- Current time and Spawnindex.
  local Tnow=timer.getTime()
  local sdx=rat:GetSpawnIndexFromGroup(group)
  
  -- Departure and destination names.
  local departure=rat.ratcraft[sdx].departure:GetName()
  local destination=rat.ratcraft[sdx].destination:GetName()
  local landing=rat.ratcraft[sdx].landing
  local WPholding=rat.ratcraft[sdx].wpholding
  local WPfinal=rat.ratcraft[sdx].wpfinal
  
  
  -- For messages
  local text
  
  -- Info on passing waypoint.
  text=string.format("Flight %s passing waypoint #%d %s.", group:GetName(), wp, rat.waypointdescriptions[wp])
  BASE.T(rat, RAT.id..text)
    
  -- New status.
  local status=rat.waypointstatus[wp]
  rat:_SetStatus(group, status)
    
  if wp==WPholding then
  
    -- Aircraft arrived at holding point
    text=string.format("Flight %s to %s ATC: Holding and awaiting landing clearance.", group:GetName(), destination)
    MESSAGE:New(text, 10):ToAllIf(rat.reportstatus)
     
    -- Register aircraft at ATC.
    if rat.ATCswitch then
      if rat.f10menu then
        MENU_MISSION_COMMAND:New("Clear for landing", rat.Menu[rat.SubMenuName].groups[sdx], rat.ClearForLanding, rat, group:GetName())
      end
      rat._ATCRegisterFlight(rat, group:GetName(), Tnow)
    end
  end
  
  if wp==WPfinal then
    text=string.format("Flight %s arrived at final destination %s.", group:GetName(), destination)
    MESSAGE:New(text, 10):ToAllIf(rat.reportstatus)
    BASE.T(rat, RAT.id..text)
  
    if landing==RAT.wp.air then
      text=string.format("Activating despawn switch for flight %s! Group will be detroyed soon.", group:GetName())
      MESSAGE:New(text, 10):ToAllIf(rat.Debug)
      BASE.T(rat, RAT.id..text)
      -- Enable despawn switch. Next time the status function is called, the aircraft will be despawned.
      rat.ratcraft[sdx].despawnme=true
    end
  end
end

--- Task function.
-- @param #RAT self
-- @param #string FunctionString Name of the function to be called.
function RAT:_TaskFunction(FunctionString, ... )
  self:F2({FunctionString, arg})
  
  local DCSTask
  local ArgumentKey
  
  -- Templatename and anticipated name the group will get
  local templatename=self.templategroup:GetName()
  local groupname=self:_AnticipatedGroupName()
  
  local DCSScript = {}
  DCSScript[#DCSScript+1] = "local MissionControllable = GROUP:FindByName(\""..groupname.."\") "
  DCSScript[#DCSScript+1] = "local RATtemplateControllable = GROUP:FindByName(\""..templatename.."\") "

  if arg and arg.n > 0 then
    ArgumentKey = '_' .. tostring(arg):match("table: (.*)")
    self.templategroup:SetState(self.templategroup, ArgumentKey, arg)
    DCSScript[#DCSScript+1] = "local Arguments = RATtemplateControllable:GetState(RATtemplateControllable, '" .. ArgumentKey .. "' ) "
    DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable, unpack( Arguments ) )"
  else
    DCSScript[#DCSScript+1] = FunctionString .. "( MissionControllable )"
  end
  
  DCSTask = self.templategroup:TaskWrappedAction(self.templategroup:CommandDoScript(table.concat(DCSScript)))

  return DCSTask
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()
  self:F()
  
  -- 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 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(RAT.id..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(RAT.id..text)
  
  if #idx>0 and nactive<self.activate_max then
  
    -- Randomly pick on group, which is activated.
    local index=idx[math.random(#idx)]
    
    -- Get corresponding group.
    local group=self.ratcraft[index].group --Wrapper.Group#GROUP
    
    -- Start aircraft.
    self:_CommandStartUncontrolled(group)
  end

end

--- Start uncontrolled aircraft group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group to be activated.
function RAT:_CommandStartUncontrolled(group)
  self:F(group)

  -- Start command.
  local StartCommand = {id = 'Start', params = {}}
  
  -- Debug message
  local text=string.format("Uncontrolled: Activating group %s.", group:GetName())
  self:T(RAT.id..text)
  
  -- Activate group.
  group:SetCommand(StartCommand)
  
  -- Spawn index.
  local index=self:GetSpawnIndexFromGroup(group)
  
  -- Set status to active.
  self.ratcraft[index].active=true
  
  -- Set status to "Ready and Starting Engines".
  self:_SetStatus(group, RAT.status.EventBirth)
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(RAT.id..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


--- 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 trigger zone defined in the mission editor exists.
-- @param #RAT self
-- @param #string name
-- @return #boolean True if zone exsits, false otherwise. 
function RAT:_ZoneExists(name)
  local z=trigger.misc.getZone(name)
  if z then            
    return true
  end
  return false
end

--- Set ROE for a group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group for which the ROE is set.
-- @param #string roe ROE of group.
function RAT:_SetROE(group, roe)
  self:T(RAT.id.."Setting ROE to "..roe.." for group "..group:GetName())
  if self.roe==RAT.ROE.returnfire then
    group:OptionROEReturnFire()
  elseif self.roe==RAT.ROE.weaponfree then
    group:OptionROEWeaponFree()
  else
    group:OptionROEHoldFire()
  end
end


--- Set ROT for a group.
-- @param #RAT self
-- @param Wrapper.Group#GROUP group Group for which the ROT is set.
-- @param #string rot ROT of group.
function RAT:_SetROT(group, rot)
  self:T(RAT.id.."Setting ROT to "..rot.." for group "..group:GetName())
  if self.rot==RAT.ROT.passive then
    group:OptionROTPassiveDefense()
  elseif self.rot==RAT.ROT.evade then
    group:OptionROTEvadeFire()
  else
    group:OptionROTNoReaction()
  end
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(RAT.id.."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(RAT.id..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 #number index Spawn index of group.
function RAT:_PlaceMarkers(waypoints, index)
  for i=1,#waypoints do
    self:_SetMarker(self.waypointdescriptions[i], waypoints[i], index)
    if self.Debug then
      local text=string.format("Marker at waypoint #%d: %s for flight #%d", i, self.waypointdescriptions[i], index)
      self:T2(RAT.id..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(RAT.id..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.
-- @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)
  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:GetDesc().category      
    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 then
        -- This is used in the SPAWN:SpawnWithIndex() function. Some values are overwritten there!
        self.SpawnUnControlled=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(RAT.id..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)
        elseif parkingdata~=nil then
          -- Parking data explicitly set by user as input parameter.
          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(RAT.id..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(RAT.id..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(RAT.id..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(RAT.id..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(RAT.id..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(RAT.id..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(RAT.id..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).
        local parkingdata=departure:GetParkingSpotsTable(termtype)
        self:T2(RAT.id..string.format("Parking at %s, terminal type %s:", departure:GetName(), tostring(termtype)))
        for _,_spot in pairs(parkingdata) do        
          self:T2(RAT.id..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(RAT.id..string.format("%s at %s: free parking spots = %d - number of units = %d", self.alias, departure: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 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(RAT.id..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(RAT.id..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


--- new
                   
      -- 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
          
          -- Sh�ps and FARPS seem to have a build in queue.
          if spawnonship or spawnonfarp or spawnonrunway or automatic then
            self:T(RAT.id..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(RAT.id..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(RAT.id..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(RAT.id..string.format("RAT group %s unit number %d: Parking    = %s",self.alias, UnitID, tostring(UnitTemplate.parking)))
        self:T2(RAT.id..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 coaltion 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

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Initializes the ATC arrays and starts schedulers.
-- @param #RAT self
-- @param #table airports_map List of all airports of the map.
function RAT:_ATCInit(airports_map)
  if not RAT.ATC.init then
    local text
    text="Starting RAT ATC.\nSimultanious = "..RAT.ATC.Nclearance.."\n".."Delay        = "..RAT.ATC.delay
	  self:T(RAT.id..text)
    RAT.ATC.init=true
    for _,ap in pairs(airports_map) do
      local name=ap:GetName()
      RAT.ATC.airport[name]={}
      RAT.ATC.airport[name].queue={}
      RAT.ATC.airport[name].busy=false
      RAT.ATC.airport[name].onfinal={}
      RAT.ATC.airport[name].Nonfinal=0
      RAT.ATC.airport[name].traffic=0
      RAT.ATC.airport[name].Tlastclearance=nil
    end
    SCHEDULER:New(nil, RAT._ATCCheck, {self}, 5, 15)
    SCHEDULER:New(nil, RAT._ATCStatus, {self}, 5, 60)
    RAT.ATC.T0=timer.getTime()
  end
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)
  self:T(string.format("%sATC %s: Adding flight %s with destination %s.", RAT.id, dest, name, dest))
  RAT.ATC.flight[name]={}
  RAT.ATC.flight[name].destination=dest
  RAT.ATC.flight[name].Tarrive=-1
  RAT.ATC.flight[name].holding=-1
  RAT.ATC.flight[name].Tonfinal=-1
end

--- Deletes a flight from ATC lists after it landed.
-- @param #RAT self
-- @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
      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)
  self:T(RAT.id.."Flight ".. name.." registered at ATC for landing clearance.")
  RAT.ATC.flight[name].Tarrive=time
  RAT.ATC.flight[name].holding=0
end


--- ATC status report about flights.
-- @param #RAT self
function RAT:_ATCStatus()

  -- Current time.
  local Tnow=timer.getTime()
   
  for name,_ in pairs(RAT.ATC.flight) do

    -- Holding time at destination.
    local hold=RAT.ATC.flight[name].holding
    local dest=RAT.ATC.flight[name].destination
    
    if hold >= 0 then
    
      -- Some string whether the runway is busy or not.
      local busy="Runway state is unknown"
      if RAT.ATC.airport[dest].Nonfinal>0 then
        busy="Runway is occupied by "..RAT.ATC.airport[dest].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)
      self:T(RAT.id..text)
      
    elseif hold==RAT.ATC.onfinal then
    
      -- Aircarft is on final approach for landing.
      local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal
      
      local text=string.format("ATC %s: Flight %s is on final. Waiting %i:%02d for landing event.", dest, name, Tfinal/60, Tfinal%60)
      self:T(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
      self:E(RAT.id.."ERROR: Unknown holding time in RAT:_ATCStatus().")
    end
  end
  
end

--- Main ATC function. Updates the landing queue of all airports and inceases holding time for all flights.
-- @param #RAT self
function RAT:_ATCCheck()

  -- Init queue of flights at all airports.
  RAT:_ATCQueue()
  
  -- Current time.
  local Tnow=timer.getTime()
  
  for name,_ in pairs(RAT.ATC.airport) do
    
    for qID,flight in ipairs(RAT.ATC.airport[name].queue) do
    
      -- Number of aircraft in queue.
      local nqueue=#RAT.ATC.airport[name].queue
    
      -- Conditions to clear an aircraft for landing
      local landing1
      if RAT.ATC.airport[name].Tlastclearance then
        -- Landing if time is enough and less then two planes are on final.
        landing1=(Tnow-RAT.ATC.airport[name].Tlastclearance > RAT.ATC.delay) and RAT.ATC.airport[name].Nonfinal < RAT.ATC.Nclearance
      else
        landing1=false
      end
      -- No other aircraft is on final.
      local landing2=RAT.ATC.airport[name].Nonfinal==0


      if not landing1 and not landing2 then
      
        -- Update holding time.
        RAT.ATC.flight[flight].holding=Tnow-RAT.ATC.flight[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.", name, flight,qID, nqueue, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60)
        self:T(RAT.id..text)
        
      else
      
        local text=string.format("ATC %s: Flight %s was cleared for landing. Your holding time was %i:%02d.", name, flight, RAT.ATC.flight[flight].holding/60, RAT.ATC.flight[flight].holding%60)
        self:T(RAT.id..text)
      
        -- Clear flight for landing.
        RAT:_ATCClearForLanding(name, flight)
        
      end
      
    end
        
  end
  
  -- Update queue of flights at all airports.
  RAT:_ATCQueue()
  
end

--- Giving landing clearance for aircraft by setting user flag.
-- @param #RAT self
-- @param #string airport Name of destination airport.
-- @param #string flight Group name of flight, which gets landing clearence.
function RAT:_ATCClearForLanding(airport, flight)
  -- Flight is cleared for landing.
  RAT.ATC.flight[flight].holding=RAT.ATC.onfinal
  -- Airport runway is busy now.
  RAT.ATC.airport[airport].busy=true
  -- Flight which is landing.
  RAT.ATC.airport[airport].onfinal[flight]=flight
  -- Number of planes on final approach.
  RAT.ATC.airport[airport].Nonfinal=RAT.ATC.airport[airport].Nonfinal+1
  -- Last time an aircraft got landing clearance.
  RAT.ATC.airport[airport].Tlastclearance=timer.getTime()  
  -- Current time.
  RAT.ATC.flight[flight].Tonfinal=timer.getTime()
  -- Set user flag to 1 ==> stop condition for holding.
  trigger.action.setUserFlag(flight, 1)
  local flagvalue=trigger.misc.getUserFlag(flight)
  
  -- Debug message.
  local text1=string.format("ATC %s: Flight %s cleared for landing (flag=%d).", airport, flight, flagvalue)
  local text2=string.format("ATC %s: Flight %s you are cleared for landing.", airport, flight)
  BASE:T( RAT.id..text1)
  MESSAGE:New(text2, 10):ToAllIf(RAT.ATC.messages)
end

--- Takes care of organisational stuff after a plane has landed.
-- @param #RAT self
-- @param #string name Group name of flight.
function RAT:_ATCFlightLanded(name)

  if RAT.ATC.flight[name] then
  
    -- Destination airport.
    local dest=RAT.ATC.flight[name].destination
    
    -- Times for holding and final approach.
    local Tnow=timer.getTime()
    local Tfinal=Tnow-RAT.ATC.flight[name].Tonfinal
    local Thold=RAT.ATC.flight[name].Tonfinal-RAT.ATC.flight[name].Tarrive
    
    -- Airport is not busy any more.
    RAT.ATC.airport[dest].busy=false
    
    -- No aircraft on final any more.
    RAT.ATC.airport[dest].onfinal[name]=nil
    
    -- Decrease number of aircraft on final.
    RAT.ATC.airport[dest].Nonfinal=RAT.ATC.airport[dest].Nonfinal-1
    
    -- Remove this flight from list of flights.
    RAT:_ATCDelFlight(RAT.ATC.flight, name)
    
    -- Increase landing counter to monitor traffic.
    RAT.ATC.airport[dest].traffic=RAT.ATC.airport[dest].traffic+1
    
    -- Number of planes landing per hour.
    local TrafficPerHour=RAT.ATC.airport[dest].traffic/(timer.getTime()-RAT.ATC.T0)*3600
    
    -- Debug info
    local text1=string.format("ATC %s: Flight %s landed. Tholding = %i:%02d, Tfinal = %i:%02d.", dest, name, Thold/60, Thold%60, Tfinal/60, Tfinal%60)
    local text2=string.format("ATC %s: Number of flights still on final %d.", dest, RAT.ATC.airport[dest].Nonfinal)
    local text3=string.format("ATC %s: Traffic report: Number of planes landed in total %d. Flights/hour = %3.2f.", dest, RAT.ATC.airport[dest].traffic, TrafficPerHour)
    local text4=string.format("ATC %s: Flight %s landed. Welcome to %s.", dest, name, dest)
    BASE:T(RAT.id..text1)
    BASE:T(RAT.id..text2)
    BASE:T(RAT.id..text3)
    MESSAGE:New(text4, 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.
-- @param #RAT self
function RAT:_ATCQueue()

  for airport,_ in pairs(RAT.ATC.airport) do
  
    -- Local airport queue.
    local _queue={}

    -- Loop over all flights.
    for name,_ in pairs(RAT.ATC.flight) do
      --fvh
      local Tnow=timer.getTime()
      
      -- Update holding time (unless holing is set to onfinal=-100)
      if RAT.ATC.flight[name].holding>=0 then
        RAT.ATC.flight[name].holding=Tnow-RAT.ATC.flight[name].Tarrive
      end
      local hold=RAT.ATC.flight[name].holding
      local dest=RAT.ATC.flight[name].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
    
    --fvh
    --for k,v in ipairs(RAT.ATC.airport[airport].queue) do
      --print(string.format("queue #%02i flight \"%s\" holding %d seconds",k, v, RAT.ATC.flight[v].holding))
    --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.
-- 
-- ## 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={},
  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:E(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.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.
  local delay=delay or 5

  -- 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)

  -- Start scheduler.
  SCHEDULER:New(nil, self._Start, {self}, delay)
  
  return self
end

--- Instantly starts the RAT manager and spawns the initial random number RAT groups for each RAT object.
-- @param #RATMANAGER self
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:_Start()

  -- 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)
    end
  end
  
  -- Start activation scheduler for uncontrolled aircraft.
  for i=1,self.nrat do      
    if self.rat[i].uncontrolled and self.rat[i].activate_uncontrolled then
      -- Start activating stuff but not before the latest spawn has happend.
      local Tactivate=math.max(time+1, self.rat[i].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)
    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:E(text)
  
  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
  self:E(string.format(RATMANAGER.id.."Manager will be stopped in %d seconds.", delay))
  SCHEDULER:New(nil, self._Stop, {self}, delay)
  return self
end

--- Instantly stops the RAT manager by terminating its scheduler.
-- @param #RATMANAGER self
-- @return #RATMANAGER RATMANAGER self object.
function RATMANAGER:_Stop()
  self:E(string.format(RATMANAGER.id.."Stopping manager with scheduler ID %s.", self.managerid))
  self.manager:Stop(self.managerid)
  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.
  local text=string.format("Number of alive groups %d. New groups to be spawned %d.", ntot, self.ntot-ntot)
  self:T(RATMANAGER.id..text)
  
  -- 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
      SCHEDULER:New(nil, RAT._SpawnWithRoute, {self.rat[i]}, time)
    end
  end
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", self.name[i], n)
    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
    N[#N+1]=0
    M[#M+1]=math.max(alive[i], min[i])
    P[#P+1]=math.max(min[i]-alive[i],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]
  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, min=%d, mini=%d, maxi=%d, add=%d, sumN=%d, sumP=%d", j, alive[j], 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, min=%d, mini=%d, maxi=%d, add=%d\n", self.name[i], i, alive[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](https://forums.eagle.ru/member.php?u=112175), 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](http://www.476vfightergroup.com/downloads.php?do=file&id=287) is highly recommended for this class.
-- 
-- ## Features:
--
--   * Impact points of bombs, rockets and missils 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 practices.
--   * 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 report at the range can be reported via radio menu.
-- 
-- More information and examples can be found below.
-- 
-- ===
-- 
-- ### [MOOSE YouTube Channel](https://www.youtube.com/channel/UCjrA9j5LQoWsG4SpS8i79Qg) 
-- ### [MOOSE - On the Range - Demonstration Video](https://www.youtube.com/watch?v=kIXcxNB9_3M)
-- 
-- ===
-- 
-- ### Author: **[funkyfranky](https://forums.eagle.ru/member.php?u=115026)**
-- 
-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536), [Ciribob](https://forums.eagle.ru/member.php?u=112175)
-- 
-- ===
-- @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 send as messages on the screen.
-- @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 #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 Indiviual 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 threashold [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 above ground 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 AGL in meters at which illumination bombs are fired. Default is 500 m.
-- @field #number illuminationmaxalt Maximum altitude AGL in meters 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 eventmoose If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler. Default true.
-- @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.
-- @extends Core.Base#BASE

--- 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" defindes 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 spector 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 findes 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 has to be given as a lua table which contains the names of @{Wrapper.Unit} or @{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 while the parameter *heading* defines its direction.
--   If the parameter *heading* is passed as **nil**, the heading is automatically taken from the heading of the first target unit as defined in the ME.
--   The parameter *inverseheading* turns the heading around by 180 degrees. This is sometimes useful, since 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 achive 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* has to be a lua table, which contains the names of @{Wrapper.Unit} and/or @{Static} objects defined in the mission editor.
--   Note that the @{Range} logic **automatically** determines, if a name belongs to a @{Wrapper.Unit} or @{Static} object now.
-- * The (optional) parameter *goodhitrange* specifies the radius around the target. If a bomb or rocket falls at a distance smaller than this number, the 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. 
--   
-- 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.
-- 
-- ## 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..." --> "Fxx. On the Range..." --> "F1. Your Range Name...".
--
-- The range menu contains the following submenues:
-- 
-- * "F1. Mark Targets": Various ways to mark targets. 
-- * "F2. My Settings": Player specific settings.
-- * "F3. Stats" Player: statistics and scores.
-- * "Range Information": Information about the range, such as bearing and range. Also range and player specific settings are displayed.
-- * "Weather Report": Temperatur, wind and QFE pressure information is provided. 
-- 
-- ## 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")
--      
--      -- 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.
--      GoldwaterRange:AddStrafePit(strafepit_left, 3000, 300, nil, true, 20, fouldist)
--      GoldwaterRange:AddStrafePit(strafepit_right, nil, nil, nil, true, nil, fouldist)
--      
--      -- 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. @{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,
  rangename=nil,
  location=nil,
  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,
  eventmoose=true,
  trackbombs=true,
  trackrockets=true,
  trackmissiles=true,
}

--- Default range parameters.
-- @list Defaults
RANGE.Defaults={
  goodhitrange=25,
  strafemaxalt=914,
  dtBombtrack=0.005,
  Tmsg=30,
  ndisplayresult=10,
  rangeradius=5000,
  TdelaySmoke=3.0,
  boxlength=3000,
  boxwidth=300,
  goodpass=20,
  goodhitrange=25,
  foulline=610,
}

--- Global list of all defined range names.
-- @field #table Names
RANGE.Names={}

--- Main radio menu.
-- @field #table MenuF10
RANGE.MenuF10={}

--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
RANGE.id="RANGE | "

--- Range script version.
-- @field #string version
RANGE.version="1.2.1"

--TODO list:
--TODO: Add custom weapons, which can be specified by the user.
--TODO: Check if units are still alive.
--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.
-- @return #RANGE RANGE object.
function RANGE:New(rangename)
  BASE:F({rangename=rangename})

  -- Inherit BASE.
  local self=BASE:Inherit(self, BASE: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"
  
  -- Debug info.
  local text=string.format("RANGE script version %s - creating new RANGE object of name: %s.", RANGE.version, self.rangename)
  self:E(RANGE.id..text)
  MESSAGE:New(text, 10):ToAllIf(self.Debug)
    
  -- Return object.
  return self
end

--- Initializes number of targets and location of the range. Starts the event handlers.
-- @param #RANGE self
function RANGE:Start()
  self:F()

  -- 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=_target.target:GetCoordinate() --Core.Point#COORDINATE
    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.rangename, self.nstrafetargets, self.nbombtargets)
    self:E(RANGE.id..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:E(RANGE.id..text)
  MESSAGE:New(text,10):ToAllIf(self.Debug)
  
  -- Event handling.
  if self.eventmoose then
    -- Events are handled my MOOSE.
    self:T(RANGE.id.."Events are handled by MOOSE.")
    self:HandleEvent(EVENTS.Birth)
    self:HandleEvent(EVENTS.Hit)
    self:HandleEvent(EVENTS.Shot)
  else
    -- Events are handled directly by DCS.
    self:T(RANGE.id.."Events are handled directly by DCS.")
    world.addEventHandler(self)
  end
  
  -- Make bomb target move randomly within the range zone.
  for _,_target in pairs(self.bombingTargets) do

    -- Check if it is a static object.
    local _static=self:_CheckStatic(_target.target:GetName())
    
    if _target.move and _static==false and _target.speed>1 then
      local unit=_target.target --Wrapper.Unit#UNIT
      _target.target:PatrolZones({self.rangezone}, _target.speed*0.75, "Off road")
    end
    
  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
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User Functions

--- 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 AGL in meters. Default is 914 m= 3000 ft.
function RANGE:SetMaxStrafeAlt(maxalt)
  self.strafemaxalt=maxalt or RANGE.Defaults.strafemaxalt
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.
function RANGE:SetBombtrackTimestep(dt)
  self.dtBombtrack=dt or RANGE.Defaults.dtBombtrack
end

--- Set time how long (most) messages are displayed.
-- @param #RANGE self
-- @param #number time Time in seconds. Default is 30 s.
function RANGE:SetMessageTimeDuration(time)
  self.Tmsg=time or RANGE.Defaults.Tmsg
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.
function RANGE:SetMessageToExaminer(examinergroupname, exclusively)
  self.examinergroupname=examinergroupname
  self.examinerexclusive=exclusively
end

--- Set max number of player results that are displayed.
-- @param #RANGE self
-- @param #number nmax Number of results. Default is 10.
function RANGE:SetDisplayedMaxPlayerResults(nmax)
  self.ndisplayresult=nmax or RANGE.Defaults.ndisplayresult
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.
function RANGE:SetRangeRadius(radius)
  self.rangeradius=radius*1000 or RANGE.Defaults.rangeradius
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.
function RANGE:SetBombtrackThreshold(distance)
  self.BombtrackThreshold=distance*1000 or 25*1000
end

--- Set range location. If this is not done, one (random) unit position of the range is used to determine the center of the range.  
-- @param #RANGE self
-- @param Core.Point#COORDINATE coordinate Coordinate of the center of the range.
function RANGE:SetRangeLocation(coordinate)
  self.location=coordinate
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.
function RANGE:SetRangeLocation(zone)
  self.rangezone=zone
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.
function RANGE:SetBombTargetSmokeColor(colorid)
  self.BombSmokeColor=colorid or SMOKECOLOR.Red
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.
function RANGE:SetStrafeTargetSmokeColor(colorid)
  self.StrafeSmokeColor=colorid or SMOKECOLOR.Green
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.
function RANGE:SetStrafePitSmokeColor(colorid)
  self.StrafePitSmokeColor=colorid or SMOKECOLOR.White
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.
function RANGE:SetSmokeTimeDelay(delay)
  self.TdelaySmoke=delay or RANGE.Defaults.TdelaySmoke
end

--- Enable debug modus.
-- @param #RANGE self
function RANGE:DebugON()
  self.Debug=true
end

--- Disable debug modus.
-- @param #RANGE self
function RANGE:DebugOFF()
  self.Debug=false
end

--- Enables tracking of all bomb types. Note that this is the default setting.
-- @param #RANGE self
function RANGE:TrackBombsON()
  self.trackbombs=true
end

--- Disables tracking of all bomb types.
-- @param #RANGE self
function RANGE:TrackBombsOFF()
  self.trackbombs=false
end

--- Enables tracking of all rocket types. Note that this is the default setting.
-- @param #RANGE self
function RANGE:TrackRocketsON()
  self.trackrockets=true
end

--- Disables tracking of all rocket types.
-- @param #RANGE self
function RANGE:TrackRocketsOFF()
  self.trackrockets=false
end

--- Enables tracking of all missile types. Note that this is the default setting.
-- @param #RANGE self
function RANGE:TrackMissilesON()
  self.trackmissiles=true
end

--- Disables tracking of all missile types.
-- @param #RANGE self
function RANGE:TrackMissilesOFF()
  self.trackmissiles=false
end


--- Add new strafe 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 #table targetnames Table of unit or static names defining the strafe targets. The first target in the list 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.
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(RANGE.id..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(RANGE.id..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(RANGE.id..text)
      MESSAGE:New(text, 10):ToAllIf(self.Debug)
          
    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(RANGE.id..text)
    MESSAGE:New(text, 10):ToAllIf(self.Debug)
    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()
    
  -- Add zone to table.
  table.insert(self.strafeTargets, {name=_name, polygon=_polygon, coordinate= Ccenter, goodPass=goodpass, targets=_targets, foulline=foulline, smokepoints=p, heading=heading})
  
  -- 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(RANGE.id..text)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)
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.
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

end

--- Add bombing target(s) to range.
-- @param #RANGE self
-- @param #table targetnames Table containing 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.
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(RANGE.id..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(RANGE.id..string.format("Adding unit bombing target %s with hit range %d.", name, goodhitrange, randommove))
      self:AddBombingTargetUnit(_unit, goodhitrange)
    else
      self:E(RANGE.id..string.format("ERROR! Could not find bombing target %s.", name))
    end
    
  end
end

--- Add a unit or static object as bombing target.
-- @param #RANGE self
-- @param Wrapper.Positionable#POSITIONABLE unit Positionable (unit or static) of the strafe 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.
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(RANGE.id..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(RANGE.id..string.format("Adding UNIT bombing target %s with good hit range %d. Random move = %s.", name, goodhitrange, tostring(randommove)))
  else
    self:E(RANGE.id..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
  
  -- Insert target to table.
  table.insert(self.bombingTargets, {name=name, target=unit, goodhitrange=goodhitrange, move=randommove, speed=speed})
end

--- Add all units of a group as bombing targets.
-- @param #RANGE self
-- @param Wrapper.Group#GROUP group Group of bombing targets.
-- @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.
function RANGE:AddBombingTargetGroup(group, goodhitrange, randommove)
  self:F({group=group, goodhitrange=goodhitrange, randommove=randommove})
  
  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
  
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(RANGE.id..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(RANGE.id..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(RANGE.id..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(RANGE.id..string.format("Foul line distance = %.1f m.", fouldist))
  return fouldist
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Handling

--- General event handler.
-- @param #RANGE self
-- @param #table Event DCS event table.
function RANGE:onEvent(Event)
  self:F3(Event)

  if Event == nil or Event.initiator == nil then
    self:T3("Skipping onEvent. Event or Event.initiator unknown.")
    return true
  end
  if Unit.getByName(Event.initiator:getName()) == nil then
    self:T3("Skipping onEvent. Initiator unit name unknown.")
    return true
  end

  local DCSiniunit = Event.initiator
  local DCStgtunit = Event.target
  local DCSweapon  = Event.weapon

  local EventData={}
  local _playerunit=nil
  local _playername=nil
  
  if Event.initiator then
    EventData.IniUnitName  = Event.initiator:getName()
    EventData.IniDCSGroup  = Event.initiator:getGroup()
    EventData.IniGroupName = Event.initiator:getGroup():getName()
    -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. 
    _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName)  
  end

  if Event.target then  
    EventData.TgtUnitName  = Event.target:getName()
    EventData.TgtUnit      = UNIT:FindByName(EventData.TgtUnitName)
  end
  
  if Event.weapon then
    EventData.Weapon         = Event.weapon
    EventData.weapon         = Event.weapon
    EventData.WeaponTypeName = Event.weapon:getTypeName()
  end  
  
  -- Event info.
  self:T3(RANGE.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id)))
  self:T3(RANGE.id..string.format("EVENT: Ini unit   = %s" , tostring(EventData.IniUnitName)))
  self:T3(RANGE.id..string.format("EVENT: Ini group  = %s" , tostring(EventData.IniGroupName)))
  self:T3(RANGE.id..string.format("EVENT: Ini player = %s" , tostring(_playername)))
  self:T3(RANGE.id..string.format("EVENT: Tgt unit   = %s" , tostring(EventData.TgtUnitName)))
  self:T3(RANGE.id..string.format("EVENT: Wpn type   = %s" , tostring(EventData.WeaponTypeName)))
    
  -- Call event Birth function.
  if Event.id==world.event.S_EVENT_BIRTH and _playername then
    self:OnEventBirth(EventData)
  end
  
  -- Call event Shot function.
  if Event.id==world.event.S_EVENT_SHOT and _playername and Event.weapon then
    self:OnEventShot(EventData)
  end
  
  -- Call event Hit function.
  if Event.id==world.event.S_EVENT_HIT and _playername and DCStgtunit then
    self:OnEventHit(EventData)
  end
  
end


--- Range event handler for event birth.
-- @param #RANGE self
-- @param Core.Event#EVENTDATA EventData
function RANGE:OnEventBirth(EventData)
  self:F({eventbirth = EventData})
  
  local _unitName=EventData.IniUnitName  
  local _unit, _playername=self:_GetPlayerUnitAndName(_unitName)
  
  self:T3(RANGE.id.."BIRTH: unit   = "..tostring(EventData.IniUnitName))
  self:T3(RANGE.id.."BIRTH: group  = "..tostring(EventData.IniGroupName))
  self:T3(RANGE.id.."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(RANGE.id..text)
    MESSAGE:New(text, 5):ToAllIf(self.Debug)
    
    self:_GetAmmo(_unitName)
    
    -- Reset current strafe status.
    self.strafeStatus[_uid] = nil
  
    -- Add Menu commands.
    self:_AddF10Commands(_unitName)
    
    -- By default, some bomb impact points and do not flare each hit on target.
    self.PlayerSettings[_playername]={}
    self.PlayerSettings[_playername].smokebombimpact=true
    self.PlayerSettings[_playername].flaredirecthits=false
    self.PlayerSettings[_playername].smokecolor=SMOKECOLOR.Blue
    self.PlayerSettings[_playername].flarecolor=FLARECOLOR.Red
    self.PlayerSettings[_playername].delaysmoke=true
  
    -- Start check in zone timer.
    if self.planes[_uid] ~= true then
      SCHEDULER:New(nil, self._CheckInZone, {self, EventData.IniUnitName}, 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(RANGE.id.."HIT: Ini unit   = "..tostring(EventData.IniUnitName))
  self:T3(RANGE.id.."HIT: Ini group  = "..tostring(EventData.IniGroupName))
  self:T3(RANGE.id.."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]

  -- Player has rolled in on a strafing target.
  if _currentTarget 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
            local text=string.format("%s, Invalid hit!\nYou already passed foul line distance of %d m for target %s.", self:_myname(_unitName), _d, targetname)
            self:_DisplayMessageToGroup(_unit, text, 10)
            self:T2(RANGE.id..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
      
        -- Position of target.
        local targetPos = _target:GetCoordinate()
      
        -- Message to player.
        --local text=string.format("%s, direct hit on target %s.", self:_myname(_unitName), targetname)
        --self:DisplayMessageToGroup(_unit, text, 10, true)
      
        -- Flare target.
        if self.PlayerSettings[_playername].flaredirecthits then
          targetPos:Flare(self.PlayerSettings[_playername].flarecolor)
        end
        
      end
    end
  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})
  
  -- 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:T(RANGE.id.."EVENT SHOT: Range "..self.rangename)
  self:T(RANGE.id.."EVENT SHOT: Ini unit    = "..EventData.IniUnitName)
  self:T(RANGE.id.."EVENT SHOT: Ini group   = "..EventData.IniGroupName)
  self:T(RANGE.id.."EVENT SHOT: Weapon type = ".._weapon)
  self:T(RANGE.id.."EVENT SHOT: Weapon name = ".._weaponName)
  
  -- Special cases:
  local _viggen=string.match(_weapon, "ROBOT") or string.match(_weapon, "RB75") or string.match(_weapon, "BK90") or string.match(_weapon, "RB15") or string.match(_weapon, "RB04")
  
  -- Tracking conditions for bombs, rockets and missiles.
  local _bombs=string.match(_weapon, "weapons.bombs") 
  local _rockets=string.match(_weapon, "weapons.nurs") 
  local _missiles=string.match(_weapon, "weapons.missiles") or _viggen
  
  -- Check if any condition applies here.
  local _track = (_bombs and self.trackbombs) or (_rockets and self.trackrockets) or (_missiles and self.trackmissiles)
    
  -- Get unit name.
  local _unitName = EventData.IniUnitName
  
  -- Get player unit and name.
  local _unit, _playername = self:_GetPlayerUnitAndName(_unitName)

  -- 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(RANGE.id..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.
  if _track and dPR<=self.BombtrackThreshold then

    -- Tracking info and init of last bomb position.
    self:T(RANGE.id..string.format("RANGE %s: Tracking %s - %s.", self.rangename, _weapon, EventData.weapon:getName()))
    
    -- Init bomb position.
    local _lastBombPos = {x=0,y=0,z=0}
        
    -- Function monitoring the position of a bomb until impact.
    local function trackBomb(_ordnance)

      -- When the pcall returns a failure the weapon has hit.
      local _status,_bombPos =  pcall(
      function()
        return _ordnance:getPoint()
      end)

      self:T3(RANGE.id..string.format("Range %s: Bomb still in air: %s", self.rangename, tostring(_status)))
      if _status then
      
        -- Still in the air. Remember this position.
        _lastBombPos = {x = _bombPos.x, y = _bombPos.y, z= _bombPos.z }

        -- Check again in 0.005 seconds.
        return timer.getTime() + self.dtBombtrack
        
      else
      
        -- Bomb did hit the ground.
        -- Get closet target to last position.
        local _closetTarget = nil
        local _distance = nil
        local _hitquality = "POOR"
        
        -- Get callsign.
        local _callsign=self:_myname(_unitName)
                  
        -- Coordinate of impact point.
        local impactcoord=COORDINATE:NewFromVec3(_lastBombPos)
        
        -- Distance from range. We dont want to smoke targets outside of the range.
        local impactdist=impactcoord:Get2DDistance(self.location)
        
        -- Smoke impact point of bomb.
        if self.PlayerSettings[_playername].smokebombimpact and impactdist<self.rangeradius then
          if self.PlayerSettings[_playername].delaysmoke then
            timer.scheduleFunction(self._DelayedSmoke, {coord=impactcoord, color=self.PlayerSettings[_playername].smokecolor}, timer.getTime() + self.TdelaySmoke)
          else
            impactcoord:Smoke(self.PlayerSettings[_playername].smokecolor)
          end
        end
            
        -- Loop over defined bombing targets.
        for _,_bombtarget in pairs(self.bombingTargets) do

          local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE
          
          if _target and _target:IsAlive() then
          
            -- Distance between bomb and target.
            local _temp = impactcoord:Get2DDistance(_target:GetCoordinate())
  
            -- Find closest target to last known position of the bomb.
            if _distance == nil or _temp < _distance then
              _distance = _temp
              _closetTarget = _bombtarget
              if _distance <= 0.5*_bombtarget.goodhitrange then
                _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 <= self.scorebombdistance then

          -- Init bomb player results.
          if not self.bombPlayerResults[_playername] then
            self.bombPlayerResults[_playername]  = {}
          end

          -- Local results.
          local _results =  self.bombPlayerResults[_playername]
          
          -- Add to table.
          table.insert(_results, {name=_closetTarget.name, distance =_distance, weapon = _weaponName, quality=_hitquality })

          -- Send message to player.
          local _message = string.format("%s, impact %d m from bullseye of target %s. %s hit.", _callsign, _distance, _closetTarget.name, _hitquality)

          -- Send message.
          self:_DisplayMessageToGroup(_unit, _message, nil, true)
        elseif _distance <= self.rangeradius then
          -- Send message
          local _message=string.format("%s, weapon fell more than %.1f km away from nearest range target. No score!", _callsign, self.scorebombdistance/1000)
          self:_DisplayMessageToGroup(_unit, _message, nil, true)
        end
        
        --Terminate the timer
        self:T(RANGE.id..string.format("Range %s, player %s: Terminating bomb track timer.", self.rangename, _playername))
        return nil

      end -- _status check
      
    end -- end function trackBomb

    -- Weapon is not yet "alife" just yet. Start timer in one second.
    self:T(RANGE.id..string.format("Range %s, player %s: Tracking of weapon starts in one second.", self.rangename, _playername))
    timer.scheduleFunction(trackBomb, EventData.weapon, timer.getTime() + 1.0)
    
  end --if _track (string.match) and player-range distance < threshold.
  
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Display Messages

--- Start smoking a coordinate with a delay.
-- @param #table _args Argements passed.
function RANGE._DelayedSmoke(_args)
  trigger.action.smoke(_args.coord:GetVec3(), _args.color)
end

--- 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 = 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.hits > b.hits end
      table.sort(_results,_sort)
  
      -- Prepare message of best results.
      local _bestMsg = ""
      local _count = 1
      
      -- Loop over results
      for _,_result in pairs(_results) do
  
        -- Message text.
        _message = _message..string.format("\n[%d] Hits %d - %s - %s", _count, _result.hits, _result.zone.name, _result.text)
      
        -- Best result.
        if _bestMsg == "" then 
          _bestMsg = string.format("Hits %d - %s - %s", _result.hits, _result.zone.name, _result.text)
        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)
  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 = 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.hits > _best.hits 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.hits, _best.zone.name, _best.text)
        table.insert(_playerResults,{msg = text, hits = _best.hits})
      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)
  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 = 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 = ""
      local _count = 1
      for _,_result in pairs(_results) do
  
        -- Message with name, weapon and distance.
        _message = _message.."\n"..string.format("[%d] %d m - %s - %s - %s hit", _count, _result.distance, _result.name, _result.weapon, _result.quality)
  
        -- Store best/first result.
        if _bestMsg == "" then
            _bestMsg = string.format("%d m - %s - %s - %s hit",_result.distance,_result.name,_result.weapon, _result.quality)
        end
  
        -- Best 10 runs only.
        if _count == self.ndisplayresult then
          break
        end
  
        -- Increase counter.
        _count = _count+1
      end
  
      -- Message.
      _message = _message .."\n\nBEST: ".._bestMsg
    end
  
    -- Send message.
    self:_DisplayMessageToGroup(_unit, _message, nil, true)
  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 = 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)
  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 = 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
    
      -- Direction vector from current position (coord) to target (position).
      local position=self.location --Core.Point#COORDINATE
      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 settings=_DATABASE:GetPlayerSettings(playername) or _SETTINGS --Core.Settings#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("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)
      text=text..texthit
      text=text..textbomb
      text=text..textdelay
      
      -- Send message to player group.
      self:_DisplayMessageToGroup(unit, text, nil, true)
      
      -- Debug output.
      self:T2(RANGE.id..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 = 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 _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE
      if _target and _target:IsAlive() then
      
        -- Core.Point#COORDINATE
        local coord=_target:GetCoordinate() --Core.Point#COORDINATE
        local mycoord=coord:ToStringA2G(_unit, _settings)
        _text=_text..string.format("\n- %s: %s",_bombtarget.name, mycoord)
      end
    end
    
    self:_DisplayMessageToGroup(_unit,_text, nil, true)
  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 = 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: %s - heading %03d",_strafepit.name, mycoord, heading)
    end
    
    self:_DisplayMessageToGroup(_unit,_text, nil, true)
  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 = 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.CelciusToFarenheit(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)
    
    -- Debug output.
    self:T2(RANGE.id..text)
  else
    self:T(RANGE.id..string.format("ERROR! Could not find player unit in RangeInfo! Name = %s", _unitname))
  end      
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Timer Functions

--- 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)

  if _unit and _playername then

    -- Current position of player unit.
    local _unitID  = _unit:GetID()

    -- Currently strafing? (strafeStatus is nil if not)
    local _currentStrafeRun = self.strafeStatus[_unitID]

    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
      
      local unitheading  = _unit:GetHeading()
      local pitheading   = _currentStrafeRun.zone.heading - 180
      local deltaheading = unitheading-pitheading
      local towardspit   = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90
      local unitalt=_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight()       
      
      -- Check if unit is inside zone and below max height AGL.
      local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit
      
      -- Debug output
      local text=string.format("Checking stil in zone. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _unitName, _playername, tostring(unitinzone), unitalt, deltaheading)
      self:T2(RANGE.id..text)
    
      -- 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)
          
        else
        
          -- Get current ammo.
          local _ammo=self:_GetAmmo(_unitName)
        
          -- Result.
          local _result = self.strafeStatus[_unitID]

          -- Judge this pass. Text is displayed on summary.
          if _result.hits >= _result.zone.goodPass*2 then
            _result.text = "EXCELLENT PASS"    
          elseif _result.hits >= _result.zone.goodPass then
            _result.text = "GOOD PASS"
          elseif _result.hits >= _result.zone.goodPass/2 then
            _result.text = "INEFFECTIVE PASS"
          else
            _result.text = "POOR PASS"
          end
          
          -- 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
          end
              
          -- Message text.      
          local _text=string.format("%s, %s with %d hits on target %s.", self:_myname(_unitName), _result.text, _result.hits, _result.zone.name)
          if shots and accur then
            _text=_text..string.format("\nTotal rounds fired %d. Accuracy %.1f %%.", shots, accur)
          end
          
          -- Send message.
          self:_DisplayMessageToGroup(_unit, _text)
    
          -- 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
        
        -- Get the current approach zone and check if player is inside.
        local zonenname=_targetZone.name
        local zone=_targetZone.polygon  --Core.Zone#ZONE_POLYGON_BASE
      
        -- Check if player is in zone and below max alt and flying towards the target.
        local unitheading  = _unit:GetHeading()
        local pitheading   = _targetZone.heading - 180
        local deltaheading = unitheading-pitheading
        local towardspit   = math.abs(deltaheading)<=90 or math.abs(deltaheading-360)<=90
        local unitalt      =_unit:GetHeight()-_unit:GetCoordinate():GetLandHeight()       
      
        -- Check if unit is inside zone and below max height AGL.
        local unitinzone=_unit:IsInZone(zone) and unitalt <= self.strafemaxalt and towardspit
           
        -- Debug info.
        local text=string.format("Checking zone %s. Unit = %s, player = %s in zone = %s. alt = %d, delta heading = %d", _targetZone.name, _unitName, _playername, tostring(unitinzone), unitalt, deltaheading)
        self:T2(RANGE.id..text)
        
        -- 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 = _targetZone, time = 1, ammo=_ammo, pastfoulline=false }
  
          -- Rolling in!
          local _msg=string.format("%s, rolling in on strafe pit %s.", self:_myname(_unitName), _targetZone.name)
          
          -- Send message.
          self:_DisplayMessageToGroup(_unit, _msg, 10, true)

          -- 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
  
        -- Main F10 menu: F10/On the Range/<Range Name>/
        if RANGE.MenuF10[_gid] == nil then
          RANGE.MenuF10[_gid]=missionCommands.addSubMenuForGroup(_gid, "On the Range")
        end
        local _rangePath    = missionCommands.addSubMenuForGroup(_gid, self.rangename, RANGE.MenuF10[_gid])
        local _statsPath    = missionCommands.addSubMenuForGroup(_gid, "Statistics",   _rangePath)
        local _markPath     = missionCommands.addSubMenuForGroup(_gid, "Mark Targets", _rangePath)
        local _settingsPath = missionCommands.addSubMenuForGroup(_gid, "My Settings",  _rangePath)
        local _infoPath     = missionCommands.addSubMenuForGroup(_gid, "Range Info",   _rangePath)
        -- F10/On the Range/<Range Name>/My Settings/
        local _mysmokePath  = missionCommands.addSubMenuForGroup(_gid, "Smoke Color", _settingsPath)
        local _myflarePath  = missionCommands.addSubMenuForGroup(_gid, "Flare Color", _settingsPath)

        -- F10/On the Range/<Range Name>/Mark Targets/
        missionCommands.addCommandForGroup(_gid, "Mark On Map",         _markPath, self._MarkTargetsOnMap, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Illuminate Range",    _markPath, self._IlluminateBombTargets, self, _unitName)        
        missionCommands.addCommandForGroup(_gid, "Smoke Strafe Pits",   _markPath, self._SmokeStrafeTargetBoxes, self, _unitName)        
        missionCommands.addCommandForGroup(_gid, "Smoke Strafe Tgts",   _markPath, self._SmokeStrafeTargets, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Smoke Bomb Tgts",     _markPath, self._SmokeBombTargets, self, _unitName)
        -- F10/On the Range/<Range Name>/Stats/
        missionCommands.addCommandForGroup(_gid, "All Strafe Results",  _statsPath, self._DisplayStrafePitResults, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "All Bombing Results", _statsPath, self._DisplayBombingResults, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "My Strafe Results",   _statsPath, self._DisplayMyStrafePitResults, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "My Bomb Results",     _statsPath, self._DisplayMyBombingResults, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Reset All Stats",     _statsPath, self._ResetRangeStats, self, _unitName)
        -- F10/On the Range/<Range Name>/My Settings/Smoke Color/
        missionCommands.addCommandForGroup(_gid, "Blue Smoke",          _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Blue)
        missionCommands.addCommandForGroup(_gid, "Green Smoke",         _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Green)
        missionCommands.addCommandForGroup(_gid, "Orange Smoke",        _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Orange)
        missionCommands.addCommandForGroup(_gid, "Red Smoke",           _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.Red)
        missionCommands.addCommandForGroup(_gid, "White Smoke",         _mysmokePath, self._playersmokecolor, self, _unitName, SMOKECOLOR.White)
        -- F10/On the Range/<Range Name>/My Settings/Flare Color/
        missionCommands.addCommandForGroup(_gid, "Green Flares",        _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Green)
        missionCommands.addCommandForGroup(_gid, "Red Flares",          _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Red)
        missionCommands.addCommandForGroup(_gid, "White Flares",        _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.White)
        missionCommands.addCommandForGroup(_gid, "Yellow Flares",       _myflarePath, self._playerflarecolor, self, _unitName, FLARECOLOR.Yellow)
        -- F10/On the Range/<Range Name>/My Settings/
        missionCommands.addCommandForGroup(_gid, "Smoke Delay On/Off",  _settingsPath, self._SmokeBombDelayOnOff, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Smoke Impact On/Off",  _settingsPath, self._SmokeBombImpactOnOff, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Flare Hits On/Off",    _settingsPath, self._FlareDirectHitsOnOff, self, _unitName)        
        -- F10/On the Range/<Range Name>/Range Information
        missionCommands.addCommandForGroup(_gid, "General Info",        _infoPath, self._DisplayRangeInfo, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Weather Report",      _infoPath, self._DisplayRangeWeather, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Bombing Targets",     _infoPath, self._DisplayBombTargets, self, _unitName)
        missionCommands.addCommandForGroup(_gid, "Strafe Pits",         _infoPath, self._DisplayStrafePits, self, _unitName)
      end
    else
      self:T(RANGE.id.."Could not find group or group ID in AddF10Menu() function. Unit name: ".._unitName)
    end
  else
    self:T(RANGE.id.."Player unit does not exist in AddF10Menu() function. Unit name: ".._unitName)
  end

end
    
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Helper Functions

--- 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(RANGE.id..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(RANGE.id..text)
          MESSAGE:New(text, 10):ToAllIf(self.Debug)
        else
          local text=string.format("Player %s has %d ammo of type %s", playername, Nammo, Tammo)
          self:T(RANGE.id..text)
          MESSAGE:New(text, 10):ToAllIf(self.Debug)
        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
  if _unitName then
    group=UNIT:FindByName(_unitName):GetGroup()
  end
  
  -- Mark bomb targets.
  for _,_bombtarget in pairs(self.bombingTargets) do
    local _target=_bombtarget.target --Wrapper.Positionable#POSITIONABLE
    if _target and _target:IsAlive() then
      local coord=_target:GetCoordinate() --Core.Point#COORDINATE
      if group then
        coord:MarkToGroup("Bomb target ".._bombtarget.name, group)
      else
        coord:MarkToAll("Bomb target ".._bombtarget.name)
      end
    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)
        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
    if _target and _target:IsAlive() then
      local coord=_target:GetCoordinate() --Core.Point#COORDINATE
      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)
  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.
function RANGE:_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 and not self.examinerexclusive then
    if _clear == true then
      trigger.action.outTextForGroup(_gid, _text, _time, _clear)
    else
      trigger.action.outTextForGroup(_gid, _text, _time)
    end
  end

  if self.examinergroupname~=nil then
    local _examinerid=GROUP:FindByName(self.examinergroupname):GetID()
    if _examinerid then
      if _clear == true then
        trigger.action.outTextForGroup(_examinerid, _text, _time, _clear)
      else
        trigger.action.outTextForGroup(_examinerid, _text, _time)
      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 = 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.PlayerSettigs[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)
  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 = 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.PlayerSettigs[playername].delaysmoke=true
      text=string.format("%s, %s, delayed smoke of bombs is now ON.", self.rangename, playername)
    end
    self:_DisplayMessageToGroup(unit, text, 5)
  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 = 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)
  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
    if _target and _target:IsAlive() then
      local coord = _target:GetCoordinate() --Core.Point#COORDINATE
      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)
    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("unkown 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("unkown 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(RANGE.id..string.format("Adding DCS static to MOOSE database. Name = %s.", name))
      _DATABASE:AddStatic(name)
    end
    
    return true
  else
    self:T3(RANGE.id..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(RANGE.id..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 nil If player does not exist.
function RANGE:_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)
    
      self:T2({DCSunit=DCSunit, unit=unit, playername=playername})
      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 a string which consits of this callsign and the player name.  
-- @param #RANGE self
-- @param #string unitname Name of the player unit.
function RANGE:_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

--- Split string. Cf http://stackoverflow.com/questions/1426954/split-string-in-lua
-- @param #RANGE self
-- @param #string str Sting to split.
-- @param #string sep Speparator for split.
-- @return #table Split text.
function RANGE:_split(str, sep)
  self:F2({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

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- **Functional (WIP)** -- 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**
-- 
-- ===
-- 
-- @module Functional.ZoneGoal
-- @image MOOSE.JPG

do -- Zone

  --- @type ZONE_GOAL
  -- @extends Core.Fsm#FSM


  -- 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",
  }
  
  --- ZONE_GOAL Constructor.
  -- @param #ZONE_GOAL self
  -- @param Core.Zone#ZONE_BASE Zone A @{Zone} object with the goal to be achieved.
  -- @return #ZONE_GOAL
  function ZONE_GOAL:New( Zone )
  
    local self = BASE:Inherit( self, FSM:New() ) -- #ZONE_GOAL
    self:F( { Zone = Zone } )

    self.Zone = Zone -- Core.Zone#ZONE_BASE
    self.Goal = GOAL:New()

    self.SmokeTime = nil

    self:AddTransition( "*", "DestroyedUnit", "*" )
  
    --- 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 Core.Zone#ZONE_BASE
  function ZONE_GOAL:GetZone()
    return self.Zone
  end
  
  
  --- Get the name of the ProtectZone
  -- @param #ZONE_GOAL self
  -- @return #string
  function ZONE_GOAL:GetZoneName()
    return self.Zone:GetName()
  end


  --- Smoke the center of theh zone.
  -- @param #ZONE_GOAL self
  -- @param #SMOKECOLOR.Color SmokeColor
  function ZONE_GOAL:Smoke( SmokeColor )
  
    self:F( { SmokeColor = SmokeColor} )
  
    self.SmokeColor = SmokeColor
  end
    
  
  --- Flare the center of the zone.
  -- @param #ZONE_GOAL self
  -- @param #SMOKECOLOR.Color FlareColor
  function ZONE_GOAL:Flare( FlareColor )
    self.Zone:FlareZone( FlareColor, math.random( 1, 360 ) )
  end


  --- When started, check the Smoke and the Zone status.
  -- @param #ZONE_GOAL self
  function ZONE_GOAL:onafterGuard()
  
    --self:GetParent( self ):onafterStart()
    
    self:F("Guard")
  
    --self:ScheduleRepeat( 15, 15, 0.1, nil, self.StatusZone, self )
    if 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})
    
    local CurrentTime = timer.getTime()
  
    if self.SmokeTime == nil or self.SmokeTime + 300 <= CurrentTime then
      if self.SmokeColor then
        self.Zone:GetCoordinate():Smoke( self.SmokeColor )
        --self.SmokeColor = nil
        self.SmokeTime = CurrentTime
      end
    end
  end


  --- @param #ZONE_GOAL self
  -- @param Core.Event#EVENTDATA EventData
  function ZONE_GOAL:__Destroyed( EventData )
    self:F( { "EventDead", EventData } )

    self:F( { EventData.IniUnit } )
    
    local Vec3 = EventData.IniDCSUnit:getPosition().p
    self:F( { Vec3 = Vec3 } )
    local ZoneGoal = self:GetZone()
    self:F({ZoneGoal})

    if EventData.IniDCSUnit then
      if ZoneGoal: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 that models processes to achieve goals involving a Zone for a Coalition.
--
-- ===
-- 
-- 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
  -- @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",
  }
  
  --- @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 @{Zone} object with the goal to be achieved.
  -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone.
  -- @return #ZONE_GOAL_COALITION
  function ZONE_GOAL_COALITION:New( Zone, Coalition )
  
    local self = BASE:Inherit( self, ZONE_GOAL:New( Zone ) ) -- #ZONE_GOAL_COALITION
    self:F( { Zone = Zone, Coalition  = Coalition  } )

    self:SetCoalition( Coalition )


    return self
  end
  

  --- Set the owning coalition of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @param DCSCoalition.DCSCoalition#coalition Coalition
  function ZONE_GOAL_COALITION:SetCoalition( Coalition )
    self.Coalition = Coalition
  end
  
  
  --- Get the owning coalition of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @return DCSCoalition.DCSCoalition#coalition Coalition.
  function ZONE_GOAL_COALITION:GetCoalition()
    return self.Coalition
  end

  
  --- Get the owning coalition name of the zone.
  -- @param #ZONE_GOAL_COALITION self
  -- @return #string Coalition name.
  function ZONE_GOAL_COALITION: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


  --- Check status Coalition ownership.
  -- @param #ZONE_GOAL_COALITION self
  function ZONE_GOAL_COALITION:StatusZone()
  
    local State = self:GetState()
    self:F( { State = self:GetState() } )
  
    self.Zone:Scan( { Object.Category.UNIT, Object.Category.STATIC } )
  
  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/CAZ%20-%20Capture%20Zones)
-- 
-- ===
-- 
-- # 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
-- 
-- ===
-- 
-- @module Functional.ZoneCaptureCoalition
-- @image Capture_Zones.JPG

do -- ZONE_CAPTURE_COALITION

  --- @type ZONE_CAPTURE_COALITION
  -- @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 @{Zone} object from one of the ZONE_ classes.  
  --     Note that ZONE_POLYGON_ classes are not yet functional.  
  --     The only functional ZONE_ classses are those derived from a ZONE_RADIUS.
  --   * 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",
  }
  
  --- @field #table ZONE_CAPTURE_COALITION.States
  ZONE_CAPTURE_COALITION.States = {}
  
  --- ZONE_CAPTURE_COALITION Constructor.
  -- @param #ZONE_CAPTURE_COALITION self
  -- @param Core.Zone#ZONE Zone A @{Zone} object with the goal to be achieved.
  -- @param DCSCoalition.DCSCoalition#coalition Coalition The initial coalition owning the zone.
  -- @return #ZONE_CAPTURE_COALITION
  -- @usage
  -- 
  --  AttackZone = ZONE:New( "AttackZone" )
  --
  --  ZoneCaptureCoalition = ZONE_CAPTURE_COALITION:New( AttackZone, coalition.side.RED ) -- 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 )
  
    local self = BASE:Inherit( self, ZONE_GOAL_COALITION:New( Zone, Coalition ) ) -- #ZONE_CAPTURE_COALITION

    self:F( { Zone = Zone, Coalition  = Coalition } )

    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

    return self
  end
  

  --- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:onenterCaptured()
  
    self:GetParent( self, ZONE_CAPTURE_COALITION ).onenterCaptured( self )
    
    self.Goal:Achieved()
  end


  function ZONE_CAPTURE_COALITION:IsGuarded()
  
    local IsGuarded = self.Zone:IsAllInZoneOfCoalition( self.Coalition )
    self:F( { IsGuarded = IsGuarded } )
    return IsGuarded
  end


  function ZONE_CAPTURE_COALITION:IsEmpty()
  
    local IsEmpty = self.Zone:IsNoneInZone()
    self:F( { IsEmpty = IsEmpty } )
    return IsEmpty
  end


  function ZONE_CAPTURE_COALITION:IsCaptured()
  
    local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition )
    self:F( { IsCaptured = IsCaptured } )
    return IsCaptured
  end
  
  
  function ZONE_CAPTURE_COALITION:IsAttacked()
  
    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
    self:F( { IsAttacked = IsAttacked } )
    return IsAttacked
  end
  
  

  --- Mark.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION: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( "Coalition: Blue\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State )  
      self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Blue\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State )
    else
      self.MarkRed = Coord:MarkToCoalitionRed( "Coalition: Red\nGuard Zone: " .. ZoneName .. "\nStatus: " .. State )  
      self.MarkBlue = Coord:MarkToCoalitionBlue( "Coalition: Red\nCapture Zone: " .. ZoneName .. "\nStatus: " .. State )  
    end
  end

  --- Bound.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION: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_CAPTURE_COALITION:onenterCaptured()
  
    --self:GetParent( self ):onenterCaptured()

    local NewCoalition = self.Zone:GetScannedCoalition()
    self:F( { NewCoalition = NewCoalition } )
    self:SetCoalition( NewCoalition )
  
    self:Mark()
  end
  
  
  function ZONE_CAPTURE_COALITION:onenterEmpty()

    --self:GetParent( self ):onenterEmpty()
  
    self:Mark()
  end
  
  
  function ZONE_CAPTURE_COALITION:onenterAttacked()
  
    --self:GetParent( self ):onenterAttacked()
  
    self:Mark()
  end


  --- When started, check the Coalition status.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION: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
  end


  function ZONE_CAPTURE_COALITION:IsCaptured()
  
    local IsCaptured = self.Zone:IsAllInZoneOfOtherCoalition( self.Coalition )
    self:F( { IsCaptured = IsCaptured } )
    return IsCaptured
  end
  
  
  function ZONE_CAPTURE_COALITION:IsAttacked()
  
    local IsAttacked = self.Zone:IsSomeInZoneOfCoalition( self.Coalition )
    self:F( { IsAttacked = IsAttacked } )
    return IsAttacked
  end
  

  --- Check status Coalition ownership.
  -- @param #ZONE_CAPTURE_COALITION self
  function ZONE_CAPTURE_COALITION:StatusZone()
  
    local State = self:GetState()
    self:F( { State = self:GetState() } )
  
    self:GetParent( self, ZONE_CAPTURE_COALITION ).StatusZone( self )
    
    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

  --- 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.
  -- @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 )
  
    StartInterval = StartInterval or 15
    RepeatInterval = RepeatInterval or 15
  
    if self.ScheduleStatusZone then
      self:ScheduleStop( self.ScheduleStatusZone )
    end
    self.ScheduleStatusZone = self:ScheduleRepeat( StartInterval, RepeatInterval, 0.1, nil, self.StatusZone, 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
  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](https://forums.eagle.ru/member.php?u=115026)**
-- 
-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536)
-- 
-- ====
-- @module Functional.Arty
-- @image Artillery.JPG

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- ARTY class
-- @type ARTY
-- @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 #table targets All targets assigned.
-- @field #table moves All moves assigned.
-- @field #table 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 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 coaliton.
-- @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. 
-- @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*) contructor.
-- 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 simultanioulsly 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 denots 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 coninues 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 specifed 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 determin 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 determin 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.   
-- 
-- ## Empoying 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 addtion 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 convenctional shells left in order to fire a nuclear shell. 
-- 
-- The default explostion 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 numer 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 speficied 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 numrous 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 addessed. 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 specifing 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 insensitve*. Only exception are the battery, alias and cluster names.
-- These must be exactly the same as the names of the goups 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 immidiately 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 addessed. 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 specifing 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 reamring 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 M818"
-- Note that the name of the rearming group has to be given in quotation marks and spellt 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 M818 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",
  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,
}

--- 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.db
ARTY.db={
  ["2B11 mortar"] = {  -- type "2B11 mortar"
    minrange   = 500,  -- correct?
    maxrange   = 7000, -- 7 km
    reloadtime = 30,   -- 30 sec
  },
  ["SPH 2S1 Gvozdika"] = { -- type "SAU Gvozdika"
    minrange   = 300,      -- correct?
    maxrange   = 15000,    -- 15 km
    reloadtime = nil,      -- unknown
  },
  ["SPH 2S19 Msta"] = { --type "SAU Msta", alias "2S19 Msta"
    minrange   = 300,     -- correct?
    maxrange   = 23500,   -- 23.5 km
    reloadtime = nil,     -- unknown
  },
  ["SPH 2S3 Akatsia"] = { -- type "SAU Akatsia", alias "2S3 Akatsia"
    minrange   = 300,   -- correct?
    maxrange   = 17000, -- 17 km
    reloadtime = nil,   -- unknown
  },
  ["SPH 2S9 Nona"] = { --type "SAU 2-C9"
    minrange   = 500,   -- correct?
    maxrange   = 7000,  -- 7 km
    reloadtime = nil,   -- unknown
  },
  ["SPH M109 Paladin"] = { -- type "M-109", alias "M109"
    minrange   = 300,     -- correct?
    maxrange   = 22000,   -- 22 km
    reloadtime = nil,   -- unknown
  },
  ["SpGH Dana"] = {       -- type "SpGH_Dana"
    minrange   = 300,     -- correct?
    maxrange   = 18700,   -- 18.7 km
    reloadtime = nil,     -- unknown
  },
  ["MLRS BM-21 Grad"] = { --type "Grad-URAL", alias "MLRS BM-21 Grad"
    minrange = 5000,  --  5 km
    maxrange = 19000, -- 19 km
    reloadtime = 420, -- 7 min
  },
  ["MLRS 9K57 Uragan BM-27"] = { -- type "Uragan_BM-27"
    minrange   = 11500, -- 11.5 km
    maxrange   = 35800, -- 35.8 km
    reloadtime = 840,   -- 14 min
  },
  ["MLRS 9A52 Smerch"] = { -- type "Smerch"
    minrange   = 20000, -- 20 km
    maxrange   = 70000, -- 70 km
    reloadtime = 2160,  -- 36 min
  },
  ["MLRS M270"] = { --type "MRLS", alias "M270 MRLS"
    minrange   = 10000, -- 10 km
    maxrange   = 32000, -- 32 km
    reloadtime = 540,   -- 9 min
  },
}

--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
ARTY.id="ARTY | "

--- Arty script version.
-- @field #string version
ARTY.version="1.0.6"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

-- TODO list:
-- 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 M818, 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?
-- TODO: Handle rearming for ships. How?
-- 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.
-- TODO: Add hit event and make the arty group relocate.
-- DONE: Add illumination and smoke.

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- 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)
  BASE:F2({cargogroup=cargogroup, alias=alias})
  
  if cargogroup then
    BASE:T(ARTY.id..string.format("ARTY script version %s. Added CARGO group %s.", ARTY.version, cargogroup:GetName()))
  else
    BASE:E(ARTY.id.."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

--- 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)
  BASE:F2({group=group, alias=alias})

  -- Inherits from FSM_CONTROLLABLE
  local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #ARTY
  
  -- Check that group is present.
  if group then
    self:T(ARTY.id..string.format("ARTY script version %s. Added group %s.", ARTY.version, group:GetName()))
  else
    self:E(ARTY.id.."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(ARTY.id..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()
  
  -- Set an alias name.
  if alias~=nil then
    self.alias=tostring(alias)
  else
    self.alias=self.groupname
  end
  
  -- 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(ARTY.id.."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).
  if self.SpeedMax>1 then
    self.ismobile=true
  else
    self.ismobile=false
  end
  
  -- 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",        "*")
  
  -- 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.


  --- 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
  
  --- 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.

  --- 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.  

  
  return self
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(ARTY.id..text)
    return nil
  end
  if text~=nil then
    self:E(ARTY.id..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(ARTY.id..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 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(ARTY.id..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(ARTY.id..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.
function ARTY:SetAlias(alias)
  self:F({alias=alias})
  self.alias=tostring(alias)
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.
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(ARTY.id.."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  
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.
function ARTY:SetMinFiringRange(range)
  self:F({range=range})
  self.minrange=range*1000 or 100
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.
function ARTY:SetMaxFiringRange(range)
  self:F({range=range})
  self.maxrange=range*1000 or 1000*1000
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.
function ARTY:SetStatusInterval(interval)
  self:F({interval=interval})
  self.StatusInterval=interval or 10
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.
function ARTY:SetWaitForShotTime(waittime)
  self:F({waittime=waittime})
  self.WaitForShotTime=waittime or 300
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. 
function ARTY:SetRearmingDistance(distance)
  self:F({distance=distance})
  self.RearmingDistance=distance or 100
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 M818 transport whilst for red an unarmed Ural-375 transport can be used.
function ARTY:SetRearmingGroup(group)
  self:F({group=group})
  self.RearmingGroup=group
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.
function ARTY:SetRearmingGroupSpeed(speed)
  self:F({speed=speed})
  self.RearmingGroupSpeed=speed
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.
function ARTY:SetRearmingGroupOnRoad(onroad)
  self:F({onroad=onroad})
  if onroad==nil then
    onroad=true
  end
  self.RearmingGroupOnRoad=onroad
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.
function ARTY:SetRearmingArtyOnRoad(onroad)
  self:F({onroad=onroad})
  if onroad==nil then
    onroad=true
  end
  self.RearmingArtyOnRoad=onroad
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.
function ARTY:SetRearmingPlace(coord)
  self:F({coord=coord})
  self.RearmingPlaceCoord=coord
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. 
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
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.
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)
end

--- Report messages of ARTY group turned on. This is the default.
-- @param #ARTY self
function ARTY:SetReportON()
  self.report=true
end

--- Report messages of ARTY group turned off. Default is on.
-- @param #ARTY self
function ARTY:SetReportOFF()
  self.report=false
end

--- Turn debug mode on. Information is printed to screen.
-- @param #ARTY self
function ARTY:SetDebugON()
  self.Debug=true
end

--- Turn debug mode off. This is the default setting.
-- @param #ARTY self
function ARTY:SetDebugOFF()
  self.Debug=false
end

--- Set default speed the group is moving at if not specified otherwise.
-- @param #ARTY self
-- @param #number speed Speed in km/h.
function ARTY:SetSpeed(speed)
  self.Speed=speed
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(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..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.
function ARTY:SetShellTypes(tableofnames)
  self:F2(tableofnames)
  self.ammoshells={}
  for _,_type in pairs(tableofnames) do
    table.insert(self.ammoshells, _type)
  end
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.
function ARTY:SetRocketTypes(tableofnames)
  self:F2(tableofnames)
  self.ammorockets={}
  for _,_type in pairs(tableofnames) do
    table.insert(self.ammorockets, _type)
  end
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.
function ARTY:SetMissileTypes(tableofnames)
  self:F2(tableofnames)
  self.ammomissiles={}
  for _,_type in pairs(tableofnames) do
    table.insert(self.ammomissiles, _type)
  end
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.
function ARTY:SetTacNukeShells(n)
  self.Nukes=n
end

--- Set nuclear warhead explosion strength.
-- @param #ARTY self
-- @param #number strength Explosion strength in kilo tons TNT. Default is 0.075 kt.
function ARTY:SetTacNukeWarhead(strength)
  self.nukewarhead=strength or 0.075
  self.nukewarhead=self.nukewarhead*1000*1000 -- convert to kg TNT.
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.
function ARTY:SetIlluminationShells(n, power)
  self.Nillu=n
  self.illuPower=power or 1.0
  self.illuPower=self.illuPower * 1000000
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.
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
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.
function ARTY:SetSmokeShells(n, color)
  self.Nsmoke=n
  self.smokeColor=color or SMOKECOLOR.Red
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.
function ARTY:SetTacNukeFires(nfires, range)
  self.nukefire=true
  self.nukefires=nfires
  self.nukerange=range
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.
function ARTY:SetMarkAssignmentsOn(key, readonly)
  self.markkey=key
  self.markallow=true
  if readonly==nil then
    self.markreadonly=false
  end
end

--- Disable assigning targets by placing markers on the F10 map.
-- @param #ARTY self
function ARTY:SetMarkTargetsOff()
  self.markallow=false
  self.markkey=nil
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:E(ARTY.id..text)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)
  
  -- Get Ammo.
  self.Nammo0, self.Nshells0, self.Nrockets0, self.Nmissiles0=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.DisplayName)
  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(ARTY.id..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           = %d km/h\n", self.SpeedMax)
  text=text..string.format("Speed default       = %d 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(ARTY.id..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:E(ARTY.id..text)
  else
    self:T(ARTY.id..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=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", Nshells)
  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(ARTY.id..text)
  MESSAGE:New(text, 20):Clear():ToCoalitionIf(self.Controllable:GetCoalition(), display)
  
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event Handling
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- 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(ARTY.id.."EVENT SHOT: Ini unit    = "..EventData.IniUnitName)
  self:T3(ARTY.id.."EVENT SHOT: Ini group   = "..EventData.IniGroupName)
  self:T3(ARTY.id.."EVENT SHOT: Weapon type = ".._weapon)
  self:T3(ARTY.id.."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(ARTY.id..text)
        MESSAGE:New(text, 5):Clear():ToAllIf(self.report or self.Debug)
        
        -- Last known position of the weapon fired.
        local _lastpos={x=0, y=0, z=0}
        
        --- Track the position of the weapon if it is supposed to model a tac nuke, illumination or smoke shell.
        -- @param #table _weapon
        local function _TrackWeapon(_data)
        
          -- When the pcall status returns false the weapon has hit.
          local _weaponalive,_currpos =  pcall(
          function()
            return _data.weapon:getPoint()
          end)
          
          -- Debug
          self:T3(ARTY.id..string.format("ARTY %s: Weapon still in air: %s", self.groupname, tostring(_weaponalive)))
          
          -- Destroy weapon before impact.
          local _destroyweapon=false
          
          if _weaponalive then
            
            -- Update last position.
            _lastpos={x=_currpos.x, y=_currpos.y, z=_currpos.z}

            -- Coordinate and distance to target.
            local _coord=COORDINATE:NewFromVec3(_lastpos)            
            local _dist=_coord:Get2DDistance(_data.target.coord) 
            
            -- Debug
            self:T3(ARTY.id..string.format("ARTY %s weapon to target dist = %d m", self.groupname,_dist))
            
            if _data.target.weapontype==ARTY.WeaponType.IlluminationShells then
            
              -- Check if within distace.
              if _dist<_data.target.radius then
              
                -- Get random coordinate within certain radius of the target.
                local _cr=_data.target.coord:GetRandomCoordinateInRadius(_data.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 _data.target.weapontype==ARTY.WeaponType.SmokeShells then           
            
              if _dist<_data.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(ARTY.id..string.format("ARTY %s destroying shell, stopping timer.", self.groupname))
              
              -- Destroy weapon and stop timer.
              _data.weapon:destroy()
              return nil
            
            else

              -- TODO: Make dt input parameter.
              local dt=0.02
              
              self:T3(ARTY.id..string.format("ARTY %s tracking weapon again in %.3f seconds", self.groupname, dt))
            
              -- Check again in 0.05 seconds.
              return timer.getTime() + dt
              
            end
            
          else
 
            -- Get impact coordinate.
            local _impactcoord=COORDINATE:NewFromVec3(_lastpos)
        
            -- Create a "nuclear" explosion and blast at the impact point.
            if _weapon.weapontype==ARTY.WeaponType.TacticalNukes then
              self:T2(ARTY.id..string.format("ARTY %s triggering nuclear explosion in one second.", self.groupname))
              SCHEDULER:New(nil, ARTY._NuclearBlast, {self,_impactcoord}, 1.0)
            end
            
            -- Stop timer.
            return nil
        
          end
        
        end
        
        -- 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
        
            self:T(ARTY.id..string.format("ARTY %s: Tracking of weapon starts in two seconds.", self.groupname))
            
            local _peter={}
            _peter.weapon=EventData.weapon
            _peter.target=UTILS.DeepCopy(self.currentTarget)
            
            timer.scheduleFunction(_TrackWeapon, _peter, timer.getTime() + 2.0)
        end
               
        -- Get current ammo.
        local _nammo,_nshells,_nrockets,_nmissiles=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 illuminatin shells because we just fired one.
        if self.currentTarget.weapontype==ARTY.WeaponType.IlluminationShells then
          self.Nillu=self.Nillu-1
        end

        -- Decrease available illuminatin 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(ARTY.id..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(ARTY.id..string.format("Group %s ammo: total=%d, shells=%d, rockets=%d, missiles=%d", self.groupname, _nammo, _nshells, _nrockets, _nmissiles))
        self:T(ARTY.id..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(ARTY.id..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(ARTY.id..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.Controllable:GetCoalition()
  --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
    -- TODO: This needs to be "fixed", once DCS gives the correct numbers for x and z.
    -- 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.Controllable:GetCoalition()
    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(ARTY.id..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(ARTY.id..string.format("INFO: ARTY group %s was not addressed! Mark text:\n%s", self.groupname, Event.text))
        return
      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.Controllable:GetCoalition(), 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.Controllable:GetCoalition(), 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.IniGroupName==_name then
    
    -- Dead Unit.
    self:T2(string.format("%s: Captured dead event for unit %s.", _name, EventData.IniUnitName))
    
    -- FSM Dead event. We give one second for update of data base.
    self:__Dead(1)
  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)
  
  -- We have a cargo group ==> check if group was loaded into a carrier.
  if self.cargogroup then
    if self.cargogroup:IsLoaded() and not self:is("InTransit")  then
      -- Group is now InTransit state. Current target is canceled.
      self:T(ARTY.id..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(ARTY.id..string.format("Group %s has been unloaded from the carrier.", self.alias))      
      self:UnLoaded()
    end
  end

  -- Debug current status info.
  if self.Debug then
    self:_StatusReport()
  end

  -- Group is being transported as cargo ==> skip everything and check again in 5 seconds.  
  if self:is("InTransit") then
    self:__Status(-5)
    return
  end
  
  -- Group on the move.
  if self:is("Moving") then
    self:T2(ARTY.id..string.format("%s: Moving", Controllable:GetName()))
  end
  
  -- Group is rearming.
  if self:is("Rearming") then
    local _rearmed=self:_CheckRearmed()
    if _rearmed then
      self:T2(ARTY.id..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(ARTY.id..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(ARTY.id..string.format("%s: Rearmed ==> CombatReady", Controllable:GetName()))
      self:CombatReady()
    end
  end
  
  -- Group arrived at destination.
  if self:is("Arrived") then
    self:T2(ARTY.id..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(ARTY.id..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
      
    -- Open fire on timed target.
    self:OpenFire(_timedTarget)
  
  elseif _normalTarget then

    -- Open fire on normal target.
    self:OpenFire(_normalTarget)

  end
  
  -- Get ammo.
  local nammo, nshells, nrockets, nmissiles=self:GetAmmo()
  
  -- 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(ARTY.id..string.format("%s: OutOfAmmo ==> Rearm ==> Rearming", Controllable:GetName()))
    self:Rearm()
  end  

  -- Call status again in ~10 sec.
  self:__Status(self.StatusInterval)
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(ARTY.id..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(ARTY.id..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(ARTY.id..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 #table 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=self:GetAmmo()
  local nfire=Nammo
  local _type="shots"
  if target.weapontype==ARTY.WeaponType.Auto then
    nfire=Nammo
    _type="shots"
  elseif target.weapontype==ARTY.WeaponType.Cannon then
    nfire=Nshells
    _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
  
  -- 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(ARTY.id..text)
  MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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.
  self:_FireAtCoord(target.coord, target.radius, target.nshells, target.weapontype)
  
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(ARTY.id..text)
    MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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(ARTY.id.."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(ARTY.id..text)
  MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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(ARTY.id..string.format("%s, group is already armed to the teeth. Rearming request denied!", self.groupname))
    return false
  else
    self:T(ARTY.id..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(ARTY.id..text)
      MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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(ARTY.id..text)
      MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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(ARTY.id..text)
      MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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(ARTY.id..text)
  MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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=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(ARTY.id..text)
    MESSAGE:New(text, 10):ToCoalitionIf(self.Controllable:GetCoalition(), 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 coodinate.
  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(ARTY.id..text)
  MESSAGE:New(text, 10):ToCoalitionIf(Controllable:GetCoalition(), 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(ARTY.id..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(ARTY.id..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.
function ARTY:onafterDead(Controllable, From, Event, To)
  self:_EventFromTo("onafterDead", Event, From, To)
  
  -- Number of units left in the group.
  local units=self.Controllable:GetUnits()
  local nunits=0
  if units~=nil then
    nunits=#units
  end
    
  -- Message.
  local text=string.format("%s, one of our units just died! %d units left.", self.groupname, nunits)
  MESSAGE:New(text, 5):ToAllIf(self.Debug)
  self:T(ARTY.id..text)
      
  -- Go to stop state.
  if nunits==0 then
    self:Stop()
  end
  
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:T(ARTY.id..string.format("Stopping ARTY FSM for group %s.", 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

  -- 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)
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(ARTY.id..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(ARTY.id..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
  
--[[ 
  local ZoneNuke=ZONE_RADIUS:New("Nukezone", _coord:GetVec2(), 2000)

  -- Scan for Scenery objects.
  ZoneNuke:Scan(Object.Category.SCENERY)
  
  -- Array with all possible hideouts, i.e. scenery objects in the vicinity of the group.
  local scenery={}

  for SceneryTypeName, SceneryData in pairs(ZoneNuke: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 impact point.
      local distance= spos:Get2DDistance(_coord)

      -- Place markers on every possible scenery object.
      if self.Debug then
        local MarkerID=spos: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(SUPPRESSION.id..text)
      end
      
      -- Add to table.
      table.insert(scenery, {object=SceneryObject, distance=distance})
      
      --SceneryObject:Destroy()      
    end
  end
  
  -- Sort scenery wrt to distance from impact point.
--  local _sort = function(a,b) return a.distance < b.distance end
--  table.sort(scenery,_sort)
  
--  for _,object in pairs(scenery) do
--    local sobject=object -- Wrapper.Scenery#SCENERY
--    sobject:Destroy()
--  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)

  -- Debug message.
  local text=string.format("%s, passing waypoint %d.", group:GetName(), i)
  if final then
    text=string.format("%s, arrived at destination.", group:GetName())
  end
  arty:T(ARTY.id..text)
  
  --[[
  if final then
    MESSAGE:New(text, 10):ToCoalitionIf(group:GetCoalition(), arty.Debug or arty.report)
  else
    MESSAGE:New(text, 10):ToAllIf(arty.Debug)
  end
  ]]
  
  -- Arrived event.
  if final and arty.groupname==group:GetName() then
    arty:Arrived()
  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.
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
  
  -- Get all units.
  local units=self.Controllable:GetUnits()
  if units==nil then
    return nammo, nshells, nrockets, nmissiles
  end
    
  for _,unit in pairs(units) do
  
    if unit and unit:IsAlive() 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:E(ARTY.id..string.format("Number of weapons %d.", weapons))
          self:E({ammotable=ammotable})    
          self:E(ARTY.id.."Ammotable:")
          for id,bla in pairs(ammotable) do
            self:E({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
          
            -- 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:E(ARTY.id..text)
      else
        self:T3(ARTY.id..text)
      end
      MESSAGE:New(text, 10):ToAllIf(display)
               
    end
  end
      
  -- Total amount of ammunition.
  nammo=nshells+nrockets+nmissiles
  
  return nammo, nshells, nrockets, nmissiles
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.Controllable:GetCoalition()

  -- 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(ARTY.id..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(ARTY.id..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(ARTY.id..'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(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..string.format("Key Everyone=true."))
      
    elseif keyphrase:lower():find("irrevocable") or keyphrase:lower():find("readonly") then
    
      assignment.readonly=true
      self:T2(ARTY.id..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(ARTY.id..string.format("Key Time=%s.", val))
      
    elseif assignment.engage and key:lower():find("shot") then
    
      assignment.nshells=tonumber(val)
      self:T(ARTY.id..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(ARTY.id..string.format("Key Maxengage=%s.", val))
      
    elseif assignment.engage and key:lower():find("radius") then
    
      assignment.radius=tonumber(val)
      self:T2(ARTY.id..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(ARTY.id..string.format("Key Weapon=%s.", val))
      
    elseif (assignment.move or assignment.set) and key:lower():find("speed") then
    
      assignment.speed=tonumber(val)
      self:T2(ARTY.id..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(ARTY.id..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(ARTY.id..string.format("Key Cancel Target (before move)=true."))
      
    elseif assignment.request and keyphrase:lower():find("rearm") then
    
      assignment.requestrearming=true
      self:T2(ARTY.id..string.format("Key Request Rearming=true."))
      
    elseif assignment.request and keyphrase:lower():find("ammo") then
    
      assignment.requestammo=true
      self:T2(ARTY.id..string.format("Key Request Ammo=true."))

    elseif assignment.request and keyphrase:lower():find("target") then
    
      assignment.requesttargets=true
      self:T2(ARTY.id..string.format("Key Request Targets=true."))

    elseif assignment.request and keyphrase:lower():find("status") then
    
      assignment.requeststatus=true
      self:T2(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..string.format("Key Cancel Move=true."))

    elseif assignment.cancel and keyphrase:lower():find("rearm") then
    
      assignment.cancelrearm=true
      self:T2(ARTY.id..string.format("Key Cancel Rearm=true."))

    elseif assignment.set and keyphrase:lower():find("rearming place") then
    
      assignment.setrearmingplace=true
      self:T(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..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.Controllable:GetCoalition())
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.Controllable:GetCoalition())
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(ARTY.id.."Sorted targets wrt prio and number of engagements:")
  for i=1,#self.targets do
    local _target=self.targets[i]
    self:T3(ARTY.id..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(ARTY.id.."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(ARTY.id..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()

  for i=1,#self.targets do
    local _target=self.targets[i]
    
    self:T3(ARTY.id..string.format("Before: Target %s - in range = %s", _target.name, tostring(_target.inrange)))
    
    -- Check if target is in range.
    local _inrange,_toofar,_tooclose=self:_TargetInRange(_target)
    self:T3(ARTY.id..string.format("Inbetw: Target %s - in range = %s, toofar = %s, tooclose = %s", _target.name, tostring(_target.inrange), tostring(_toofar), tostring(_tooclose)))
    
    -- 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(ARTY.id..text)
        MESSAGE:New(text,10):ToCoalitionIf(self.Controllable:GetCoalition(), 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.Controllable:GetCoalition(), 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(ARTY.id..string.format("After: Target %s - in range = %s", _target.name, tostring(_target.inrange)))
    
  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(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..string.format("Found TIMED HIGH PRIO target %s.", self:_TargetInfo(_target)))
          return _target
        end
      else
        -- No current target.
        self:T2(ARTY.id..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(ARTY.id..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
    
      -- Debug info.
      self:T(ARTY.id..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(ARTY.id..string.format("Have target with name %s. Index = %d", targetname, i))
    if targetname==name then
      self:T2(ARTY.id..string.format("Found target with name %s. Index = %d", name, i))
      return i
    end
  end
  
  self:T2(ARTY.id..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(ARTY.id..string.format("Have move with name %s. Index = %d", movename, i))
    if movename==name then
      self:T2(ARTY.id..string.format("Found move with name %s. Index = %d", name, i))
      return i
    end
  end
  
  self:T2(ARTY.id..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=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(ARTY.id..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 _nshells==0 then
    
      self:T(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..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(ARTY.id..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=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=Nshells
  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(ARTY.id..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(ARTY.id..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(ARTY.id..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.
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:E({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(ARTY.id..text)
    MESSAGE:New(text, 5):ToCoalitionIf(self.Controllable:GetCoalition(), (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.
  if not (self.ismobile or self.iscargo) and _inrange==false then
    self:RemoveTarget(target.name)
  end

  return _inrange,_toofar,_tooclose
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(ARTY.id..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(ARTY.id..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
-- @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",
  target.name, target.prio, target.radius, target.nshells, target.engaged, target.maxengage, weapon, clock,_underfire)
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(ARTY.id..text)

    end   
  end
  
  -- Debug text.
  local text=string.format("\nLatitude  %s", tostring(_latitude))
  text=text..string.format("\nLongitude %s", tostring(_longitude))
  self:T2(ARTY.id..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(ARTY.id..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.
--
-- ===
-- 
-- ## 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](https://forums.eagle.ru/member.php?u=115026)**
-- 
-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536)
-- 
-- ===
-- 
-- @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 #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.
-- @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,
  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,
}

--- Enumerator of possible rules of engagement.
-- @field #list ROE
SUPPRESSION.ROE={
  Hold="Weapon Hold",
  Free="Weapon Free",
  Return="Return Fire",  
}

--- Enumerator of possible alarm states.
-- @field #list AlarmState
SUPPRESSION.AlarmState={
  Auto="Auto",
  Green="Green",
  Red="Red",
}

--- Main F10 menu for suppresion, i.e. F10/Suppression.
-- @field #string MenuF10
SUPPRESSION.MenuF10=nil

--- Some ID to identify who we are in output of the DCS.log file.
-- @field #string id
SUPPRESSION.id="SUPPRESSION | "

--- PSEUDOATC version.
-- @field #number version
SUPPRESSION.version="0.9.0"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--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 SUPPRESSION object.
-- @return nil If group does not exist or is not a ground group.
function SUPPRESSION:New(group)
  BASE:F2(group)

  -- Inherits from FSM_CONTROLLABLE
  local self=BASE:Inherit(self, FSM_CONTROLLABLE:New()) -- #SUPPRESSION
  
  -- Check that group is present.
  if group then
    self:T(SUPPRESSION.id..string.format("SUPPRESSION version %s. Activating suppressive fire for group %s", SUPPRESSION.version, group:GetName()))
  else
    self:E(SUPPRESSION.id.."Suppressive fire: 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(SUPPRESSION.id..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.
  local DCSgroup=Group.getByName(group:GetName())
  local DCSunit=DCSgroup:getUnit(1)
  self.DCSdesc=DCSunit:getDesc()
  
  -- Get max speed the group can do and convert to km/h.
  self.SpeedMax=self.DCSdesc.speedMaxOffRoad*3.6
  
  -- Set speed to maximum.
  self.Speed=self.SpeedMax
  
  -- Is this infantry or not.
  self.IsInfantry=DCSunit: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("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("*",           "Dead",      "*")
  
  self:AddTransition("TakingCover", "Hit",       "TakingCover")
  self:AddTransition("FallingBack", "Hit",       "FallingBack")

  --- 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 OnAfer "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.
  

  --- 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.


  --- 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.


  --- 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.


  --- 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.


  --- 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.


  --- 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.


  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(SUPPRESSION.id..string.format("Set ave suppression time to %d seconds.", self.Tsuppress_ave))
  self:T(SUPPRESSION.id..string.format("Set min suppression time to %d seconds.", self.Tsuppress_min))
  self:T(SUPPRESSION.id..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:Status(message)

  local name=self.Controllable:GetName()
  local nunits=#self.Controllable:GetUnits()
  local roe=self.CurrentROE
  local state=self.CurrentAlarmState
  local life_min, life_max, life_ave, life_ave0, groupstrength=self:_GetLife()
  
  local text=string.format("Status of group %s\n", name)
  text=text..string.format("Number of units: %d of %d\n", nunits, self.IniGroupStrength)
  text=text..string.format("Current state: %s\n", self:GetState())
  text=text..string.format("ROE: %s\n", roe)  
  text=text..string.format("Alarm state: %s\n", state)
  text=text..string.format("Hits taken: %d\n", self.Nhit)
  text=text..string.format("Life min: %3.0f\n", life_min)
  text=text..string.format("Life max: %3.0f\n", life_max)
  text=text..string.format("Life ave: %3.0f\n", life_ave)
  text=text..string.format("Life ave0: %3.0f\n", life_ave0)
  text=text..string.format("Group strength: %3.0f", groupstrength)
  
  MESSAGE:New(text, 10):ToAllIf(message or self.Debug)
  self:T(SUPPRESSION.id.."\n"..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())
  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(SUPPRESSION.id..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

end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Before "Hit" event. (Of course, this is not really before the group got hit.)
-- @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
function SUPPRESSION:onbeforeHit(Controllable, From, Event, To, Unit, AttackUnit)
  self:_EventFromTo("onbeforeHit", Event, From, To)
  
  --local Tnow=timer.getTime()
  --env.info(SUPPRESSION.id..string.format("Last hit = %s  %s", tostring(self.LastHit), tostring(Tnow)))
  
  return true
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(SUPPRESSION.id..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
  
  -- Give info on current status.
  if self.Debug then
    self:Status()
  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(SUPPRESSION.id..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(SUPPRESSION.id..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()
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(SUPPRESSION.id..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.Green)

  -- 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

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- 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.")
    self:T2(SUPPRESSION.id..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(SUPPRESSION.id..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)
  
  -- Number of units left in the group.
  local nunits=#self.Controllable:GetUnits()
      
  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(SUPPRESSION.id..text)
      
  -- Go to stop state.
  if nunits==0 then
    self:T(SUPPRESSION.id..string.format("Stopping SUPPRESSION for group %s.", Controllable:GetName()))
    self:Stop()
    if self.mooseevents then
      self:UnHandleEvent(EVENTS.Dead)
      self:UnHandleEvent(EVENTS.Hit)
    else
      world.removeEventHandler(self)
    end
  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:F(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(SUPPRESSION.id..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(SUPPRESSION.id..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(SUPPRESSION.id..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(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName))
    else
      self:T2(SUPPRESSION.id..string.format("Group %s: Dead MOOSE unit DOES NOT not exist! Unit name %s.", GroupNameIni, IniUnitName))
    end
    
    if EventData.IniDCSUnit then
      self:T2(SUPPRESSION.id..string.format("Group %s: Dead DCS unit DOES exist! Unit name %s.", GroupNameIni, IniUnitName))
    else
      self:T2(SUPPRESSION.id..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(SUPPRESSION.id..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(SUPPRESSION.id..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(SUPPRESSION.id..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 "Off road"
  wait=wait or 30

  local group=self.Controllable -- Wrapper.Controllable#CONTROLLABLE
  
  -- 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)
  
  -- Number of waypoints.
  local nx
  if dist <= 50 then
    nx=2
  elseif dist <= 100 then
    nx=3
  elseif dist <= 500 then
    nx=4
  else
    nx=5
  end
  
  -- Number of intermediate waypoints.
  local dx=dist/(nx-1)
    
  -- Waypoint and task arrays.
  local wp={}
  local tasks={}
  
  -- First waypoint is the current position of the group.
  wp[1]=ini:WaypointGround(speed, formation)
  tasks[1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, 1, false)

  if self.Debug then  
    local MarkerID=ini:MarkToAll(string.format("Waypoing %d of group %s (initial)", #wp, self.Controllable:GetName()))
  end
  
  self:T2(SUPPRESSION.id..string.format("Number of waypoints %d", nx))
  for i=1,nx-2 do
  
    local x=dx*i
    local coord=ini:Translate(x, heading)
    
    wp[#wp+1]=coord:WaypointGround(speed, formation)
    tasks[#tasks+1]=group:TaskFunction("SUPPRESSION._Passing_Waypoint", self, #wp, false)
    
    self:T2(SUPPRESSION.id..string.format("%d x = %4.1f", i, x))
    if self.Debug then
      local MarkerID=coord:MarkToAll(string.format("Waypoing %d of group %s", #wp, self.Controllable:GetName()))
    end
    
  end
  self:T2(SUPPRESSION.id..string.format("Total distance: %4.1f", dist))
  
  -- Final waypoint.
  wp[#wp+1]=fin:WaypointGround(speed, formation)
  if self.Debug then
    local MarkerID=fin:MarkToAll(string.format("Waypoing %d of group %s (final)", #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)

  -- Add final task.  
  tasks[#tasks+1]=group:TaskCombo(TaskComboFin)

  -- Original waypoints of the group.
  local Waypoints = group:GetTemplateRoutePoints()
  
  -- New points are added to the default route.
  for i,p in ipairs(wp) do
    table.insert(Waypoints, i, wp[i])
  end
  
  -- Set task for all waypoints.
  for i,wp in ipairs(Waypoints) do
    group:SetTaskWaypoint(Waypoints[i], tasks[i])
  end
  
  -- Submit task and route group along waypoints.
  group:Route(Waypoints)

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(SUPPRESSION.id..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(SUPPRESSION.id..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(SUPPRESSION.id.."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(SUPPRESSION.id.."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(SUPPRESSION.id..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(SUPPRESSION.id..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(SUPPRESSION.id.."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(SUPPRESSION.id..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(SUPPRESSION.id.."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(SUPPRESSION.id..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(SUPPRESSION.id..text)
end

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- **Functional** - Rudimentary 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](https://forums.eagle.ru/member.php?u=115026)**
-- 
-- ### Contributions: [FlightControl](https://forums.eagle.ru/member.php?u=89536)
-- 
-- ====
-- @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 If true, events are handled by MOOSE. If false, events are handled directly by DCS eventhandler.
-- @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",
  player={},
  Debug=false,
  mdur=30,
  mrefresh=120,
  talt=3,
  chatty=true,
  eventsmoose=true,
}

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- 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.9.1"

-----------------------------------------------------------------------------------------------------------------------------------------

-- TODO list
-- DONE: Add takeoff event.
-- DONE: Add user functions.

-----------------------------------------------------------------------------------------------------------------------------------------

--- 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:E(PSEUDOATC.id.."Starting PseudoATC")
  
  -- Handle events.
  if self.eventsmoose then
    self:T(PSEUDOATC.id.."Events are handled by MOOSE.")
    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)
    --self:HandleEvent(EVENTS.Ejection,        self._PlayerLeft)
    --self:HandleEvent(EVENTS.PilotDead,       self._PlayerLeft)
  else
    self:T(PSEUDOATC.id.."Events are handled by DCS.")
    -- Events are handled directly by DCS.
    world.addEventHandler(self)
  end
  
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

--- 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

--- 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

--- Event handler for suppressed groups.
--@param #PSEUDOATC self
--@param #table Event Event data table. Holds event.id, event.initiator and event.target etc.
function PSEUDOATC:onEvent(Event)
  if Event == nil or Event.initiator == nil or Unit.getByName(Event.initiator:getName()) == nil then
    return true
  end

  local DCSiniunit  = Event.initiator
  local DCSplace    = Event.place
  local DCSsubplace = Event.subplace

  local EventData={}
  local _playerunit=nil
  local _playername=nil
  
  if Event.initiator then
    EventData.IniUnitName  = Event.initiator:getName()
    EventData.IniDCSGroup  = Event.initiator:getGroup()
    EventData.IniGroupName = Event.initiator:getGroup():getName()  
    -- Get player unit and name. This returns nil,nil if the event was not fired by a player unit. And these are the only events we are interested in. 
    _playerunit, _playername = self:_GetPlayerUnitAndName(EventData.IniUnitName)  
  end

  if Event.place then
    EventData.Place=Event.place
    EventData.PlaceName=Event.place:getName()
  end
  if Event.subplace then
    EventData.SubPlace=Event.subplace
    EventData.SubPlaceName=Event.subplace:getName()
  end
  
  -- Event info.
  self:T3(PSEUDOATC.id..string.format("EVENT: Event in onEvent with ID = %s", tostring(Event.id)))
  self:T3(PSEUDOATC.id..string.format("EVENT: Ini unit   = %s" , tostring(EventData.IniUnitName)))
  self:T3(PSEUDOATC.id..string.format("EVENT: Ini group  = %s" , tostring(EventData.IniGroupName)))
  self:T3(PSEUDOATC.id..string.format("EVENT: Ini player = %s" , tostring(_playername)))
  self:T3(PSEUDOATC.id..string.format("EVENT: Place      = %s" , tostring(EventData.PlaceName)))
  self:T3(PSEUDOATC.id..string.format("EVENT: SubPlace   = %s" , tostring(EventData.SubPlaceName)))
  
  -- Event birth.
  if Event.id == world.event.S_EVENT_BIRTH and _playername then
    self:_OnBirth(EventData)
  end
  
  -- Event takeoff.
  if Event.id == world.event.S_EVENT_TAKEOFF and _playername and EventData.Place then
    self:_PlayerTakeOff(EventData)
  end
  
  -- Event land.
  if Event.id == world.event.S_EVENT_LAND and _playername and EventData.Place then
    self:_PlayerLanded(EventData)
  end
  
  -- Event player left unit
  if Event.id == world.event.S_EVENT_PLAYER_LEAVE_UNIT and _playername then
    self:_PlayerLeft(EventData)
  end

  -- Event crash ==> player left unit
  if Event.id == world.event.S_EVENT_CRASH and _playername then
    self:_PlayerLeft(EventData)
  end

--[[
  -- Event eject ==> player left unit
  if Event.id == world.event.S_EVENT_EJECTION and _playername then
    self:_PlayerLeft(EventData)
  end

  -- Event pilot dead ==> player left unit
  if Event.id == world.event.S_EVENT_PILOT_DEAD and _playername then
    self:_PlayerLeft(EventData)
  end
]]    
end

--- 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)
  
  -- 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)
  
  -- 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, _playername=self:_GetPlayerUnitAndName(_unitName)
  local _base=nil
  local _baseName=nil
  if EventData.place then
    _base=EventData.place
    _baseName=EventData.place:getName()
  end
--  if EventData.subplace then
--    local _subPlace=EventData.subplace
--    local _subPlaceName=EventData.subplace: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,_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()
  
  -- Init player table.  
  self.player[GID]={}
  self.player[GID].group=group
  self.player[GID].unit=unit
  self.player[GID].groupname=GroupName
  self.player[GID].unitname=UnitName
  self.player[GID].playername=PlayerName
  self.player[GID].callsign=CallSign
  self.player[GID].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"
  self.player[GID].menu_main=missionCommands.addSubMenuForGroup(GID, "Pseudo ATC")
    
  -- Create/update list of nearby airports.
  self:LocalAirports(GID)
  
  -- Create submenu of local airports.
  self:MenuAirports(GID)
  
  -- Create submenu Waypoints.
  self:MenuWaypoints(GID)
  
  -- Start scheduler to refresh the F10 menues.
  self.player[GID].scheduler, self.player[GID].schedulerid=SCHEDULER:New(nil, self.MenuRefresh, {self, GID}, 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 id=group:GetID()
  local PlayerName=self.player[id].playername
  local Callsign=self.player[id].callsign
  local UnitName=self.player[id].unitname
  local GroupName=self.player[id].groupname
  local CallSign=self.player[id].callsign
  
  -- Debug message.
  local text=string.format("Player %s in unit %s of group %s (id=%d) landed at %s.", PlayerName, UnitName, GroupName, id, place)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text, 30):ToAllIf(self.Debug)
  
  -- Stop altitude reporting timer if its activated.
  self:AltitudeTimerStop(id)
  
  -- Welcome message.
  if place and self.chatty then
    local text=string.format("Touchdown! Welcome to %s. Have a nice day!", place)
    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 id=group:GetID()
  local PlayerName=self.player[id].playername
  local Callsign=self.player[id].callsign
  local UnitName=self.player[id].unitname
  local GroupName=self.player[id].groupname
  local CallSign=self.player[id].callsign
  
  -- Debug message.
  local text=string.format("Player %s in unit %s of group %s (id=%d) took off at %s.", PlayerName, UnitName, GroupName, id, place)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text, 30):ToAllIf(self.Debug)
    
  -- Bye-Bye message.
  if place and self.chatty then
    local text=string.format("%s, %s, you are airborne. Have a safe trip!", place, CallSign)
    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 id=group:GetID()
  
  if self.player[id] then
  
    -- Debug message.
    local text=string.format("Player %s (callsign %s) of group %s just left unit %s.", self.player[id].playername, self.player[id].callsign, self.player[id].groupname, self.player[id].unitname)
    self:T(PSEUDOATC.id..text)
    MESSAGE:New(text, 30):ToAllIf(self.Debug)
    
    -- Stop scheduler for menu updates
    if self.player[id].schedulerid then
      self.player[id].scheduler:Stop(self.player[id].schedulerid)
    end
    
    -- Stop scheduler for reporting alt if it runs.
    self:AltitudeTimerStop(id)
    
    -- Remove main menu.
    if self.player[id].menu_main then
      missionCommands.removeItem(self.player[id].menu_main)
    end
  
    -- Remove player array.
    self.player[id]=nil
    
  end
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- Menu Functions

--- Refreshes all player menues.
-- @param #PSEUDOATC self.
-- @param #number id Group id of player unit. 
function PSEUDOATC:MenuRefresh(id)
  self:F({id=id})

  -- Debug message.
  local text=string.format("Refreshing menues for player %s in group %s.", self.player[id].playername, self.player[id].groupname)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text,30):ToAllIf(self.Debug)

  -- Clear menu.
  self:MenuClear(id)
  
  -- Create list of nearby airports.
  self:LocalAirports(id)
      
  -- Create submenu Local Airports.
  self:MenuAirports(id)
  
  -- Create submenu Waypoints etc.
  self:MenuWaypoints(id)
  
end


--- Clear player menus.
-- @param #PSEUDOATC self.
-- @param #number id Group id of player unit. 
function PSEUDOATC:MenuClear(id)
  self:F(id)

  -- Debug message.
  local text=string.format("Clearing menus for player %s in group %s.", self.player[id].playername, self.player[id].groupname)
  self:T(PSEUDOATC.id..text)
  MESSAGE:New(text,30):ToAllIf(self.Debug)
  
  -- Delete Airports menu.
  if self.player[id].menu_airports then
    missionCommands.removeItemForGroup(id, self.player[id].menu_airports)
    self.player[id].menu_airports=nil
  else
    self:T2(PSEUDOATC.id.."No airports to clear menus.")
  end
 
  -- Delete waypoints menu.
  if self.player[id].menu_waypoints then
    missionCommands.removeItemForGroup(id, self.player[id].menu_waypoints)
    self.player[id].menu_waypoints=nil
  end
  
  -- Delete report alt until touchdown menu command.
  if self.player[id].menu_reportalt then
    missionCommands.removeItemForGroup(id, self.player[id].menu_reportalt)
    self.player[id].menu_reportalt=nil
  end

  -- Delete request current alt menu command.
  if self.player[id].menu_requestalt then
    missionCommands.removeItemForGroup(id, self.player[id].menu_requestalt)
    self.player[id].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 id Group id of player unit for which menues are created. 
function PSEUDOATC:MenuAirports(id)
  self:F(id)

  -- Table for menu entries.
  self.player[id].menu_airports=missionCommands.addSubMenuForGroup(id, "Local Airports", self.player[id].menu_main)
   
  local i=0
  for _,airport in pairs(self.player[id].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(id, name, self.player[id].menu_airports)
    
    -- Create menu reporting commands
    missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name)
    missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name)
    
    -- Debug message.
    self:T(string.format(PSEUDOATC.id.."Creating airport menu item %s for ID %d", name, id))
  end
end

--- Create "F10/Pseudo ATC/Waypoints/<Waypoint i>  menu items.
-- @param #PSEUDOATC self
-- @param #number id Group id of player unit for which menues are created. 
function PSEUDOATC:MenuWaypoints(id)
  self:F(id)

  -- Player unit and callsign.
  local unit=self.player[id].unit --Wrapper.Unit#UNIT
  local callsign=self.player[id].callsign
  
  -- Debug info.
  self:T(PSEUDOATC.id..string.format("Creating waypoint menu for %s (ID %d).", callsign, id))
     
  if #self.player[id].waypoints>0 then
  
    -- F10/PseudoATC/Waypoints
    self.player[id].menu_waypoints=missionCommands.addSubMenuForGroup(id, "Waypoints", self.player[id].menu_main)

    local j=0    
    for i, wp in pairs(self.player[id].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)
      
      -- "F10/PseudoATC/Waypoints/Waypoint X"
      local submenu=missionCommands.addSubMenuForGroup(id, name, self.player[id].menu_waypoints)
      
      -- Menu commands for each waypoint "F10/PseudoATC/My Aircraft (callsign)/Waypoints/Waypoint X/<Commands>"
      missionCommands.addCommandForGroup(id, "Weather Report", submenu, self.ReportWeather, self, id, pos, name)
      missionCommands.addCommandForGroup(id, "Request BR", submenu, self.ReportBR, self, id, pos, name)
    end
  end
  
  self.player[id].menu_reportalt  = missionCommands.addCommandForGroup(id, "Talk me down",     self.player[id].menu_main, self.AltidudeTimerToggle, self, id)
  self.player[id].menu_requestalt = missionCommands.addCommandForGroup(id, "Request altitude", self.player[id].menu_main, self.ReportHeight, self, id)
end

-----------------------------------------------------------------------------------------------------------------------------------------
-- Reporting Functions

--- Weather Report. Report pressure QFE/QNH, temperature, wind at certain location.
-- @param #PSEUDOATC self
-- @param #number id Group id to which the report is delivered.
-- @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(id, position, location)
  self:F({id=id, position=position, location=location})
  
  -- Player unit system settings.
  local settings=_DATABASE:GetPlayerSettings(self.player[id].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.CelciusToFarenheit(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("Wind from %s at %s (%s).", Ds, Vs, Bd)
  
  -- Send message
  self:_DisplayMessageToGroup(self.player[id].unit, text, self.mdur, true)
  
end

--- Report absolute bearing and range form player unit to airport.
-- @param #PSEUDOATC self
-- @param #number id Group id to the report is delivered.
-- @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(id, position, location)
  self:F({id=id, position=position, location=location})

  -- Current coordinates.
  local unit=self.player[id].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.player[id].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 to player group.  
  MESSAGE:New(text, self.mdur):ToGroup(self.player[id].group)      
end

--- Report altitude above ground level of player unit.
-- @param #PSEUDOATC self
-- @param #number id Group id to the report is delivered.
-- @param #number dt (Optional) Duration the message is displayed.
-- @param #boolean _clear (Optional) Clear previouse messages. 
-- @return #number Altitude above ground.
function PSEUDOATC:ReportHeight(id, dt, _clear)
  self:F({id=id, 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.player[id].unit --Wrapper.Unit#UNIT
  
  if unit and unit:IsAlive() then
  
    local position=unit:GetCoordinate()
    local height=get_AGL(position)
    local callsign=unit:GetCallsign()
    
    -- Settings.
    local settings=_DATABASE:GetPlayerSettings(self.player[id].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)
    
    -- 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.player[id].unit,_text, dt,_clear)
    
    -- Return height
    return height
  end
  
  return 0        
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- Toggle report altitude reporting on/off.
-- @param #PSEUDOATC self.
-- @param #number id Group id of player unit. 
function PSEUDOATC:AltidudeTimerToggle(id)
  self:F(id)
  
  if self.player[id].altimerid then
    -- If the timer is on, we turn it off.
    self:AltitudeTimerStop(id)
  else
    -- If the timer is off, we turn it on.
    self:AltitudeTimeStart(id)
  end
end

--- Start altitude reporting scheduler.
-- @param #PSEUDOATC self.
-- @param #number id Group id of player unit. 
function PSEUDOATC:AltitudeTimeStart(id)
  self:F(id)
  
  -- Debug info.
  self:T(PSEUDOATC.id..string.format("Starting altitude report timer for player ID %d.", id))
  
  -- Start timer. Altitude is reported every ~3 seconds.
  self.player[id].altimer, self.player[id].altimerid=SCHEDULER:New(nil, self.ReportHeight, {self, id, 0.1, true}, 1, 3)
end

--- Stop/destroy DCS scheduler function for reporting altitude.
-- @param #PSEUDOATC self.
-- @param #number id Group id of player unit. 
function PSEUDOATC:AltitudeTimerStop(id)

  -- Debug info.
  self:T(PSEUDOATC.id..string.format("Stopping altitude report timer for player ID %d.", id))
  
  -- Stop timer.
  if self.player[id].altimerid then
    self.player[id].altimer:Stop(self.player[id].altimerid)
  end
  
  self.player[id].altimer=nil
  self.player[id].altimerid=nil
end

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc

--- Create list of nearby airports sorted by distance to player unit.
-- @param #PSEUDOATC self
-- @param #number id Group id of player unit.
function PSEUDOATC:LocalAirports(id)
  self:F(id)

  -- Airports table.  
  self.player[id].airports=nil
  self.player[id].airports={}
  
  -- Current player position.
  local pos=self.player[id].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 q=AIRBASE:FindByName(name):GetCoordinate()
      local d=q:Get2DDistance(pos)
      
      -- Add to table.
      table.insert(self.player[id].airports, {distance=d, name=name})
      
    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.player[id].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) assests in stock and spawns them upon request.
--    * Manages requests of assets from other warehouses.
--    * Queueing system with optional priorization 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.
--
-- === 
-- 
-- ## 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.
--
-- Please note that his class is work in progress and in an **alpha** stage.
-- 
-- ===
--
-- ### Author: **funkyfranky**
-- ### Co-author: FlightControl (cargo dispatcher classes)
--
-- ===
--
-- @module Functional.Warehouse
-- @image MOOSE.JPG

--- WAREHOUSE class.
-- @type WAREHOUSE
-- @field #string ClassName Name of the class.
-- @field #boolean Debug If true, send debug messages to all.
-- @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 coaliton.
-- @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 #string wid Identifier of the warehouse printed before other output to DCS.log file.
-- @field #number uid Unit identifier of the warehouse. Derived from id of warehouse static element.
-- @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 autosavefilename File name of the auto asset save file. Default is auto generated from warehouse id and name.
-- @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 stategic aspects since capturing/defending and destroying/protecting an enemy or your
-- own warehous 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 assest 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 modelled
-- 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.
-- 
-- ### 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.
-- 
-- 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_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 consistancy 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 warhouse.
-- * 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 priciple, 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 transporation 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 devided 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 occurr.
-- 
-- ## 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 cirular 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.
-- 
-- 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 paticular, all requests to the warehouse will
-- spawn assets beloning 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 priciple, 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 paricular 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)
-- * "*" --> "AssetDead" --> "*" (a whole asset group 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 coaliton.
-- 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 themselfs 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 attemt 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 behing 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 stategic bombers in a mission. Three B-52s are lauched 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,
  Report        =  true,
  warehouse     =   nil,
  alias         =   nil,
  zone          =   nil,
  airbase       =   nil,
  airbasename   =   nil,
  road          =   nil,
  rail          =   nil,
  spawnzone     =   nil,
  wid           =   nil,
  uid           =   nil,
  markerid      =   nil,
  dTstatus      =    30,
  queueid       =     0,
  stock         =    {},
  queue         =    {},
  pending       =    {},
  transporting  =    {},
  delivered     =    {},
  defending     =    {},
  portzone      =   nil,
  shippinglanes =    {},
  offroadpaths  =    {},
  autodefence   = false,
  spawnzonemaxdist = 5000,
  autosave      = false,
  autosavepath  =   nil,
  autosavefile  =   nil,
}

--- Item of the warehouse stock table.
-- @type WAREHOUSE.Assetitem
-- @field #number uid Unique id of the asset.
-- @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 kilo gramms.
-- @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) funktion.

--- 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}.

--- 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.
-- @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.
WAREHOUSE.Descriptor = {
  GROUPNAME="templatename",
  UNITTYPE="unittype",
  ATTRIBUTE="attribute",
  CATEGORY="category",
}

--- 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_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_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",
  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 WAREHOUSE.db
-- @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 #table Warehouses Table holding all defined @{#WAREHOUSE} objects by their unique ids.
WAREHOUSE.db = {
  AssetID    = 0,
  Assets     = {},
  Warehouses = {}
}

--- Warehouse class version.
-- @field #string version
WAREHOUSE.version="0.6.4"

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- 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.
-- TODO: 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: Test capturing a neutral warehouse.
-- 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.
-- TODO: Added habours as interface for transport to from warehouses? Could make a rudimentary shipping dispatcher.
-- TODO: Add save/load capability of warehouse <==> percistance 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 coaliton 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 of the warehouse.
-- @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  
-- @return #WAREHOUSE self
function WAREHOUSE:New(warehouse, alias)
  BASE:T({warehouse=warehouse})
  
  -- Check if just a string was given and convert to static.
  if type(warehouse)=="string" then
    warehouse=STATIC:FindByName(warehouse, true)
  end
  
  -- Nil check.
  if warehouse==nil then
    BASE:E("ERROR: Warehouse does not exist!")
    return nil
  end
  
  -- Set alias.
  self.alias=alias or warehouse:GetName()

  -- Print version.
  env.info(string.format("Adding warehouse v%s for structure %s with alias %s", WAREHOUSE.version, warehouse:GetName(), self.alias))

  -- Inherit everthing from FSM class.
  local self = BASE:Inherit(self, FSM:New()) -- #WAREHOUSE

  -- Set some string id for output to DCS.log file.
  self.wid=string.format("WAREHOUSE %s | ", self.alias)

  -- Set some variables.
  self.warehouse=warehouse
  self.uid=tonumber(warehouse:GetID())

  -- Closest of the same coalition but within a certain range.
  local _airbase=self:GetCoordinate():GetClosestAirbase(nil, self:GetCoalition())
  if _airbase and _airbase:GetCoordinate():Get2DDistance(self:GetCoordinate()) < 3000 then
    self:SetAirbase(_airbase)
  end
      
  -- Define warehouse and default spawn zone.
  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)
  
  -- Add warehouse to database.
  WAREHOUSE.db.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("Attacked",        "Request",           "*")           -- Process a request. Only in running mode.
  self:AddTransition("*",               "Unloaded",          "*")           -- Cargo has been unloaded from the carrier (unused ==> unnecessary?).
  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",              "*")           -- TODO 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("*",               "AssetDead",         "*")           -- An asset group died.
  self:AddTransition("*",               "Destroyed",         "Destroyed")   -- Warehouse was destroyed. All assets in stock are gone and warehouse is stopped.
  
  ------------------------
  --- 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 "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.


  --- 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 "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

--- 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 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


--- 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

--- 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 auto defence off. This is the default. 
-- @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


--- 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 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.wid.."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:GetCoordinate()
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:GetDesc().category
  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

--- Get warehouse unique ID from static warehouse object. This is the ID under which you find the @{#WAREHOUSE} object in the global data base.
-- @param #WAREHOUSE self
-- @param #string staticname Name of the warehouse static object.
-- @return #number Warehouse unique ID.  
function WAREHOUSE:GetWarehouseID(staticname)
  local warehouse=STATIC:FindByName(staticname, true)
  local uid=tonumber(warehouse:GetID())
  return uid
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 WAREHOUSE.db.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 coaliton 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(WAREHOUSE.db.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=WAREHOUSE.db.Assets[aid]
    self:E({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

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM states
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- On after Start event. Starts the warehouse. Addes 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("Coaliton = %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)

  -- THIS! caused aircraft to be spawned and started but they would never begin their route!
  -- VERY strange. Need to test more.
  --[[  
  -- Debug mark warehouse & spawn zone.
  self.zone:BoundZone(30, self.country)
  self.spawnzone:BoundZone(30, self.country)
  ]]
  
  --self.spawnzone:GetCoordinate():MarkToCoalition(string.format("Warehouse %s spawn zone", self.alias), self:GetCoalition())
  
  -- 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 then
    self.road:MarkToAll(string.format("%s road connection.", self.alias), 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 then
    self.rail:MarkToAll(string.format("%s rail connection.", self.alias), 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.wid..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.wid..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.wid..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)
  self:I(self.wid..string.format("Checking status of warehouse %s. Current FSM state %s. Global warehouse assets = %d.", self.alias, self:GetState(), #WAREHOUSE.db.Assets))
  
  -- Check if any pending jobs are done and can be deleted from the 
  self:_JobDone()
 
  -- Print status.
  self:_DisplayStatus()
    
  -- Check if warehouse is being attacked or has even been captured.
  self:_CheckConquered()
  
  -- 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.
  self:_PrintQueue(self.queue, "Queue waiting")
  self:_PrintQueue(self.pending, "Queue pending")

  -- 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
    
    -- 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.wid..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.
        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)
        
        -- 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.wid..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:_InfoMessage(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 -- 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.
function WAREHOUSE:onafterAddAsset(From, Event, To, group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, skill, liveries, assignment)
  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)
            self:T3(warehouse.wid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport)))
          elseif istransport==false then
            request.ndelivered=request.ndelivered+1
            request.cargogroupset:Remove(self:_GetNameWithOut(group), true)
            self:T3(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset)))
          else
            self:T(warehouse.wid..string.format("WARNING: Group %s is neither cargo nor transport!", group:GetName()))
          end
          
        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

      -- Get the asset from the global DB.
      local asset=self:FindAssetInDB(group)

      -- Set livery.      
      if liveries then
        asset.livery=liveries[math.random(#liveries)]
      end
      
      -- Set skill.
      asset.skill=skill
        
      -- 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)
        table.insert(self.stock, asset)
        self:NewAsset(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)
      
      -- Add created assets to stock of this warehouse.
      for _,asset in pairs(assets) do
        table.insert(self.stock, asset)
        self:NewAsset(asset, assignment or "")
      end      
      
    end   
    
    -- Destroy group if it is alive.
    if group:IsAlive()==true then
      self:_DebugMessage(string.format("Destroying group %s.", group:GetName()), 5)
      -- Setting parameter to false, i.e. creating NO dead or remove unit event, seems to not confuse the dispatcher logic.
      group:Destroy(false)
    end
  
  else
    self:E(self.wid.."ERROR: Unknown group added as asset!")
  end
  
  -- Update status.
  --self:__Status(-1)
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.
-- @return #table A table containing all registered assets.
function WAREHOUSE:_RegisterAsset(group, ngroups, forceattribute, forcecargobay, forceweight, loadradius, liveries, skill)
  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+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 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)
  
  -- Get weight and cargo bay size in kg.
  local weight=0
  local cargobay={}
  local cargobaytot=0
  local cargobaymax=0
  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
    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.wid..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.
    WAREHOUSE.db.AssetID=WAREHOUSE.db.AssetID+1
    
    -- Set parameters.
    asset.uid=WAREHOUSE.db.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.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
    
    if i==1 then
      self:_AssetItemInfo(asset)
    end
    
    -- Add asset to global db.
    WAREHOUSE.db.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("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:T(self.wid..text)
  self:T({DCSdesc=asset.DCSdesc})
  self:T3({Template=asset.template})
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.
-- @parma #string assignment The (optional) assignment for the asset.
function WAREHOUSE:onafterNewAsset(From, Event, To, asset, assignment)
  self:T(self.wid..string.format("New asset %s id=%d with assignment %s.", asset.templatename, asset.uid, 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
  
  else
    self:_ErrorMessage("ERROR: Invalid request. Asset descriptor is not ATTRIBUTE, CATEGORY, GROUPNAME or UNITTYPE!", 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() 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 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 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, tostring(request.assetdescval), tostring(request.nasset), request.transporttype, tostring(request.ntransport))
  self:_DebugMessage(text, 5)

  -- Update status
  --self:__Status(-1)
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.
      --TODO: Unless(!) this is a moving warehouse which could, e.g., be an aircraft carrier. 
      --self:_DeleteQueueItem(Request, self.queue)
      
      return false
    end
    
  end

  return true
end


--- On after "Request" 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.Queueitem Request Information table of the request.
function WAREHOUSE:onafterRequest(From, Event, To, Request)

  -- Info message.
  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.assetdescval)
  text=text..string.format("Transports %s of type %s.", tostring(Request.ntransport), tostring(Request.transporttype))
  self:_InfoMessage(text, 5)

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Cargo assets.
  ------------------------------------------------------------------------------------------------------------------------------------

 -- Pending request. Add cargo groups to request.
  local Pending=Request  --#WAREHOUSE.Pendingitem
  
  -- Set time stamp.
  Pending.timestamp=timer.getAbsTime()
  
  -- Init problem table.
  Pending.assetproblem={}
  
  -- Spawn assets of this request.
  local _spawngroups=self:_SpawnAssetRequest(Pending) --Core.Set#SET_GROUP
  
  -- Check if any group was spawned. If not, delete the request.
  if _spawngroups:Count()==0 then
    self:_DebugMessage(string.format("ERROR: Groups or request %d could not be spawned. Request is rejected and deleted from queue!", Request.uid))
    -- Delete request from queue.
    self:_DeleteQueueItem(Request, self.queue)    
    return
  end
  
  -- General type and category.
  local _cargotype=Request.cargoattribute    --#WAREHOUSE.Attribute
  local _cargocategory=Request.cargocategory --DCS#Group.Category
   
  -- Add groups to pending item.
  Pending.cargogroupset=_spawngroups

  ------------------------------------------------------------------------------------------------------------------------------------
  -- 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()))
    
    -- Add request to pending queue.
    table.insert(self.pending, Pending)
    
    -- Delete request from queue.
    self:_DeleteQueueItem(Request, self.queue)
        
    -- Start self request.
    self:__SelfRequest(1,_spawngroups, Pending)
    
    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.wid..string.format("Got selfpropelled request for %d assets.",_spawngroups:Count()))    

    for _,_spawngroup in pairs(_spawngroups:GetSetObjects()) do
      
      -- Group intellisense.
      local group=_spawngroup --Wrapper.Group#GROUP
            
            
      -- Route cargo to their destination.
      if _cargocategory==Group.Category.GROUND then      
        self:T2(self.wid..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.wid..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.wid..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.wid..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.wid..string.format("ERROR: unknown category %s for self propelled cargo %s!", tostring(_cargocategory), tostring(group:GetName())))
      end

    end
    
    -- Add request to pending queue.
    table.insert(self.pending, Pending)
    
    -- Delete request from queue.
    self:_DeleteQueueItem(Request, self.queue)
    
    -- No cargo transport necessary.
    return
  end

  ------------------------------------------------------------------------------------------------------------------------------------
  -- Prepare cargo groups for transport
  ------------------------------------------------------------------------------------------------------------------------------------
  
  -- Add groups to cargo if they don't go by themselfs.
  local CargoGroups --Core.Set#SET_CARGO
  
  -- 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
  end
  
  -- Empty cargo group set.
  CargoGroups = SET_CARGO:New()
  
  local function getasset(group)
    local _,aid,_=self:_GetIDsFromGroup(group)
    self:FindAssetInDB(group)
  end
  
  -- Add cargo groups to set.
  for _i,_group in pairs(_spawngroups: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
  
  ------------------------------------------------------------------------------------------------------------------------------------
  -- Transport assets and dispatchers
  ------------------------------------------------------------------------------------------------------------------------------------

  -- Set of cargo carriers.
  local TransportSet = SET_GROUP:New()

  -- Shortcut to transport assets.  
  local _assetstock=Request.transportassets
  
  -- General type and category.
  local _transporttype=Request.transportattribute
  local _transportcategory=Request.transportcategory

  -- 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  _transportcategory==Group.Category.AIRPLANE or _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
 
       -- Create an alias name with the UIDs for the sending warehouse, asset and request.
    local _alias=self:_alias(_assetitem.unittype, self.uid, _assetitem.uid, Request.uid)      
    
    local spawngroup=nil --Wrapper.Group#GROUP
    
    -- 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, Pending, 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, Pending, Parking[_assetitem.uid], false)
      
      -- Activate helo randomly within the next 10 seconds.
      --spawngroup:StartUncontrolled(math.random(10))
    
    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 then
  
      self:_ErrorMessage("ERROR: Cargo transport by ship not supported yet!")
      return
  
    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
    
    -- Check if group was spawned.
    if spawngroup then
      -- Set state of warehouse so we can retrieve it later.
      spawngroup:SetState(spawngroup, "WAREHOUSE", self)

      -- Add group to transportset.
      TransportSet:AddGroup(spawngroup)

      -- Add assets to pending request.
      Pending.assets[_assetitem.uid]=_assetitem
      
      -- Add transport assets.
      table.insert(_transportassets,_assetitem)
    end
  end

  -- Delete spawned items from warehouse stock.
  for _,_item in pairs(_transportassets) do
    self:_DeleteStockItem(_item)
  end
  
  -- Add transport groups, i.e. the carriers to request.
  Pending.transportgroupset=TransportSet
  
  -- Add cargo group set.
  Pending.transportcargoset=CargoGroups

  -- Add request to pending queue.
  table.insert(self.pending, Pending)

  -- Delete request from queue.
  self:_DeleteQueueItem(Request, self.queue)
  
  -- Adjust carrier units. This has to come AFTER the dispatchers have been defined because they set the cargobay free weight!
  Pending.carriercargo={}  
  for _,carriergroup in pairs(TransportSet: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.
      Pending.carriercargo[carrierunit:GetName()]={}
      
      -- Adjust cargo bay of carrier unit.
      local cargobay=asset.cargobay[_i]
      carrierunit:SetCargoBayWeightLimit(cargobay)
      
      -- Debug info.
      self:T2(self.wid..string.format("Cargo bay weight limit of carrier unit %s: %.1f kg.", carrierunit:GetName(), carrierunit:GetCargoBayFreeWeight()))      
    end
  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(TransportSet, 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(TransportSet, 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(TransportSet, CargoGroups, PickupZoneSet, DeployZoneSet, 0)
    
    -- Set home zone.
    CargoTransport:SetHomeZone(self.spawnzone)
        
  else
    self:E(self.wid.."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
  if self.spawnzone.Radius~=nil then
    pickupouter=self.spawnzone.Radius
    pickupinner=20
  end
  local deployouter=200
  local deployinner=0
  if self.spawnzone.Radius~=nil then
    deployouter=Request.warehouse.spawnzone.Radius
    deployinner=20
  end  
  CargoTransport:SetPickupRadius(pickupouter, pickupinner)
  CargoTransport:SetDeployRadius(deployouter, deployinner)    

  --------------------------------
  -- 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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..string.format("ERROR unloaded Cargo group is not alive!"))
  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.wid..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
    
    -- 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)
      self:T2(warehouse.wid..string.format("Transport %d of %s returned home.", request.ntransporthome, tostring(request.ntransport)))
    elseif istransport==false then
      request.ndelivered=request.ndelivered+1
      request.cargogroupset:Remove(self:_GetNameWithOut(group), true)
      self:T2(warehouse.wid..string.format("Cargo %d of %s delivered.", request.ndelivered, tostring(request.nasset)))
    else
      self:E(warehouse.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group:GetName()))
    end    
  
    -- Move asset from pending queue into new warehouse.
    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
  local text=string.format("Warehouse %s: All assets delivered to warehouse %s!", self.alias, request.warehouse.alias)
  self:_InfoMessage(text, 5)

  -- 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)
    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.wid..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


--- 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 which has captured the warehouse.
function WAREHOUSE:onafterChangeCountry(From, Event, To, Country)

  local CoalitionOld=self:GetCoalition()

  -- Respawn warehouse with new coalition/country.
  self.warehouse:ReSpawn(Country)
  
  local CoalitionNew=self:GetCoalition()
  
  -- Delete all waiting requests because they are not valid any more.
  self.queue=nil
  self.queue={}
    
  -- Airbase could have been captured before and already belongs to the new coalition.
  local airbase=AIRBASE:FindByName(self.airbasename)
  local airbasecoaltion=airbase:GetCoalition()
  
  if CoalitionNew==airbasecoaltion 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 unil it is captured.
    self.airbase=nil
  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 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)

  -- Change coalition and country of warehouse static.
  self:ChangeCoaliton(Coalition, Country)

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 "AssetDead" event triggerd 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)
  local text=string.format("Asset %s from request id=%d is dead!", asset.templatename, request.uid)
  self:T(self.wid..text)
  self:_DebugMessage(text)
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.", self.alias, #self.stock)
  self:_InfoMessage(text)
  
  -- 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

  --self.queue=nil
  --self.queue={}

  --self.stock=nil
  --self.stock={}

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.wid..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.wid..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.wid..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.
-- @return Core.Set#SET_GROUP Set of groups that were spawned.
function WAREHOUSE:_SpawnAssetRequest(Request)
  self:F2({requestUID=Request.uid})

  -- Shortcut to cargo assets.  
  local _assetstock=Request.cargoassets

  -- General type and category.
  local _cargotype=Request.cargoattribute    --#WAREHOUSE.Attribute
  local _cargocategory=Request.cargocategory --DCS#Group.Category
  
  -- 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 _cargocategory==Group.Category.AIRPLANE or _cargocategory==Group.Category.HELICOPTER then
    Parking=self:_FindParkingForAssets(self.airbase,_assetstock) or {}
  end
  
  -- Spawn aircraft in uncontrolled state.
  local UnControlled=true
  
  -- Create an empty group set.
  local _groupset=SET_GROUP:New()

  -- Table for all spawned assets.
  local _assets={}
  
  -- Loop over cargo requests.
  for i=1,#_assetstock do

    -- Get stock item.
    local _assetitem=_assetstock[i] --#WAREHOUSE.Assetitem
    
    -- Alias of the group.
    local _alias=self:_Alias(_assetitem, Request)

    -- Spawn an asset group.
    local _group=nil --Wrapper.Group#GROUP          
    if _assetitem.category==Group.Category.GROUND then
    
      -- Spawn ground troops.      
      _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.spawnzone)
      
    elseif _assetitem.category==Group.Category.AIRPLANE or _assetitem.category==Group.Category.HELICOPTER then
    
      -- Spawn air units.
      if Parking[_assetitem.uid] then
        _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, Parking[_assetitem.uid], UnControlled)
      else
        _group=self:_SpawnAssetAircraft(_alias,_assetitem, Request, nil, UnControlled)
      end
      
    elseif _assetitem.category==Group.Category.TRAIN then
    
      -- Spawn train.
      if self.rail then
        --TODO: Rail should only get one asset because they would spawn on top!
      end
      
      self:E(self.wid.."ERROR: Spawning of TRAIN assets not possible yet!")
      
    elseif _assetitem.category==Group.Category.SHIP then
    
      -- Spawn naval assets.
      _group=self:_SpawnAssetGroundNaval(_alias,_assetitem, Request, self.portzone)
      
    else
      self:E(self.wid.."ERROR: Unknown asset category!")
    end

    -- Add group to group set and asset list.
    if _group then
      _groupset:AddGroup(_group)
      table.insert(_assets, _assetitem)      
    else
      self:E(self.wid.."ERROR: Cargo asset could not be spawned!")
    end
    
  end

  -- Delete spawned items from warehouse stock.
  for _,_asset in pairs(_assets) do
    local asset=_asset --#WAREHOUSE.Assetitem
    Request.assets[asset.uid]=asset
    self:_DeleteStockItem(asset)
  end
  
  -- Overwrite the assets with the actually spawned ones.
  Request.cargoassets=_assets

  return _groupset
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 aioff If true, AI of ground units are set to off.
-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned.
function WAREHOUSE:_SpawnAssetGroundNaval(alias, asset, request, spawnzone, aioff)

  if asset and (asset.category==Group.Category.GROUND or asset.category==Group.Category.SHIP) 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()

    -- 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
    
    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
  
    -- Spawn group.
    local group=_DATABASE:Spawn(template) --Wrapper.Group#GROUP
    
    -- Activate group. Should only be necessary for late activated groups.
    --group:Activate()
    
    -- Switch AI off if desired. This works only for ground and naval groups.
    if aioff then
      group:SetAIOff()
    end
    
    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 hotstart Spawn aircraft with engines already on. Default is a cold start with engines off.
-- @return Wrapper.Group#GROUP The spawned group or nil if the group could not be spawned.
function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrolled, hotstart)

  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)
    
    -- Set route points.
    if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
    
      -- Get flight path if the group goes to another warehouse by itself.
      template.route.points=self:_GetFlightplan(asset, self.airbase, request.warehouse.airbase)
      
    else
      
      -- Cold start (default).
      local _type=COORDINATE.WaypointType.TakeOffParking
      local _action=COORDINATE.WaypointAction.FromParkingArea
      
      -- Hot start.
      if hotstart then
        _type=COORDINATE.WaypointType.TakeOffParkingHot
        _action=COORDINATE.WaypointAction.FromParkingAreaHot
      end
    
      -- 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<#template.units 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
  
        unit.parking_id = nil
        unit.parking    = nil
      
      else
    
        local coord=parking[i].Coordinate    --Core.Point#COORDINATE
        local terminal=parking[i].TerminalID --#number
        
        if self.Debug then
          coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal))
        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
      
    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
    
    -- Activate group - should only be necessary for late activated groups.
    --group:Activate()
    
    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

  -- For group units, visible needs to be false.
  if asset.category==Group.Category.GROUND then
    --template.visible=false
  end
  
  -- No late activation.
  template.lateActivation=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.
-- @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:_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.
      local ToWP=request.warehouse.spawnzone:GetRandomCoordinate():WaypointGround(_speed, "Off Road")
      table.insert(Waypoints, #Waypoints+1, ToWP)
    
    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 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.wid..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.wid..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.
    local starttime=math.random(60)
    aircraft:StartUncontrolled(starttime)
    
    -- Debug info.
    self:T2(self.wid..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

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Event handler functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--- 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.wid..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
      self:T(self.wid..string.format("Warehouse %s captured event birth of its asset unit %s.", self.alias, EventData.IniUnitName))
    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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..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)
          local istransport=self:_GroupIsTransport(group,request)
          
          -- Check if engine shutdown happend at right airbase because the event is also triggered in other situations.
          local rightairbase=group:GetCoordinate():GetClosestAirbase():GetName()==request.warehouse:GetAirbase():GetName()
    
          -- Check that group is cargo and not transport.
          if istransport==false and rightairbase then    
      
            -- Debug info.
            local text=string.format("Air asset group %s from warehouse %s arrived at its destination.", group:GetName(), self.alias)
            self:_InfoMessage(text)
            
            -- 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.
            self:__Arrived(dt, group)
            
          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.wid..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
    end
    
    --self:I(self.wid..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.wid..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, 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 #WAREHOUSE.Pendingitem request Request that needs to be updated.
function WAREHOUSE:_UnitDead(deadunit, request)

  -- Flare unit
  deadunit:FlareRed()
  
  -- Group the dead unit belongs to.
  local group=deadunit:GetGroup()
  
  -- Check if this was the last unit of the group ==> whole group dead.
  local groupdead=true
  local nunits=0
  local nunits0=0
  if group then
    -- Get current size of group and substract the unit that just died because it is not counted yet!
    nunits=group:GetSize()-1
    nunits0=group:GetInitialSize()
   
    if nunits > 0 then
      groupdead=false
    end    
  end
  
  
  -- 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(group)
  
  -- Debug message.
  local text=string.format("Unit %s died! #units=%d/%d ==> Group dead=%s (IsAlive=%s).", unitname, nunits, nunits0, tostring(groupdead), tostring(group:IsAlive()))
  self:T2(self.wid..text)

  -- Check if this really works as expected!
  if nunits<0 then
    self:E(self.wid.."ERROR: Number of units negative! This should not happen.")
  end
  
  -- Group is dead!
  if groupdead then
    self:T(self.wid..string.format("Group %s (transport=%s) is dead!", groupname, tostring(self:_GroupIsTransport(group,request))))
    if self.Debug then
      group:SmokeWhite()
    end
    -- Trigger AssetDead event.
    local asset=self:FindAssetInDB(group)
    self:AssetDead(asset, request)
  end
   
  
  -- Not sure what this does actually and if it would be better to set it to true.
  local NoTriggerEvent=true
  
  if request.transporttype==WAREHOUSE.TransportType.SELFPROPELLED then
  
    ---
    -- Easy case: Group can simply be removed from the cargogroupset.
    ---
    
    -- Remove dead group from carg group set.
    if groupdead==true then
      request.cargogroupset:Remove(groupname, NoTriggerEvent)
      self:T(self.wid..string.format("Removed selfpropelled cargo %s: ncargo=%d.", groupname, request.cargogroupset:Count()))
    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=self:_GroupIsTransport(group,request)
    
    if istransport==true 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.wid..string.format("Removed transported cargo %s inside dead carrier %s: ncargo=%d", cargoname, unitname, request.cargogroupset:Count()))
        end
        
      end
    
      -- Whole carrier group is dead. Remove it from the carrier group set.
      if groupdead then
        request.transportgroupset:Remove(groupname, NoTriggerEvent)
        self:T(self.wid..string.format("Removed transport %s: ntransport=%d", groupname, request.transportgroupset:Count()))
      end  
        
    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.
      if groupdead==true then
        request.cargogroupset:Remove(groupname, NoTriggerEvent)
        self:T(self.wid..string.format("Removed transported cargo %s outside carrier: ncargo=%d", groupname, request.cargogroupset:Count()))
        -- This as well?
        --request.transportcargoset:RemoveCargosByName(RemoveCargoNames)
      end
      
    else  
      self:E(self.wid..string.format("ERROR: Group %s is neither cargo nor transport!", group: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.wid..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.wid..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 coaltion.
        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.wid..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:GetCoordinate())
      
      -- 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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..string.format("ERROR: INVALID request. Requesting warehouse is of wrong coaltion! 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.wid..string.format("ERROR: INVALID request. Requesting warehouse is stopped!"))
      valid=false    
    end

    -- Is receiving warehouse destroyed?
    if request.warehouse:IsDestroyed() then
      self:E(self.wid..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.wid..string.format("Got invalid request id=%d.", request.uid))
      table.insert(invalid, request) 
    else
      self:T3(self.wid..string.format("Got valid request id=%d.", request.uid))
    end    
  end

  -- Delete invalid requests.
  for _,_request in pairs(invalid) do
    self:E(self.wid..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=self:_GetTerminal(asset.attribute)
        
        -- Get number of parking spots.
        local np_departure=self.airbase:GetParkingSpotsNumber(termtype)
        local np_destination=request.airbase:GetParkingSpotsNumber(termtype)
        
        -- Debug info.
        self:T(string.format("Asset attribute = %s, terminal type = %d, spots at departure = %d, destination = %d", asset.attribute, termtype, np_departure, 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, 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, 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 then
        self:E("ERROR: Incorrect request. Ground asset requested but at least one spawn zone is in water!")
        valid=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 then
    
      -- Transport by ship.
      self:E("ERROR: Incorrect request. Transport by SHIP not implemented yet!")
      valid=false
    
    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)
      
      -- 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)
      
      -- Get number of parking spots.
      local np_departure=self.airbase:GetParkingSpotsNumber(termtype)
                   
      -- Debug info.
      self:T(self.wid..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.wid..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.
        local np_destination=request.airbase:GetParkingSpotsNumber(termtype)

        -- Debug info.
        self:T(self.wid..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.wid..string.format("ERROR: Got invalid request id=%d.", request.uid))
  else
    self:T3(self.wid..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.transport)=="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.wid..text)
    return false
  end
  
  local _transports
  local _assetattribute
  local _assetcategory
  
  -- Check if at least one (cargo) asset is available.
  if _nassets>0 then

    -- Get the attibute of the requested asset.
    _assetattribute=_assets[1].attribute
    _assetcategory=_assets[1].category  
    
    -- Check available parking for air asset units.    
    if self.airbase and (_assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER) then
    
      local Parking=self:_FindParkingForAssets(self.airbase,_assets)
      
      --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then
      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
    
    -- 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 self.airbase and (_transportcategory==Group.Category.AIRPLANE or _transportcategory==Group.Category.HELICOPTER) then
        local Parking=self:_FindParkingForAssets(self.airbase,_transports)
        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
      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. Nothing to do for now.
    
    -- 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
      
    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.wid..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.wid..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)

  -- 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.wid.."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.wid..string.format("Cargo bay = %d  (unit=%d)", transport.cargobay[j], j))
    end
  end
  self:T2(self.wid..string.format("Total capacity = %d", totalbay))

  -- Total cargo weight of all assets to transports.
  self:T2(self.wid.."Cargo weight:")
  local totalcargoweight=0
  for i=1,#cargoassets do
    local asset=cargoassets[i] --#WAREHOUSE.Assetitem
    totalcargoweight=totalcargoweight+asset.weight
    self:T2(self.wid..string.format("weight = %d", asset.weight))
  end    
  self:T2(self.wid..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.wid..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.wid..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.wid..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.wid..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.wid..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.wid..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('env.info(\"WAREHOUSE: Simple task function called!\") ')
  DCSScript[#DCSScript+1] = string.format('local mygroup   = GROUP:FindByName(\"%s\") ', groupname)        -- The group that executes the task function. Very handy with the "...".
  DCSScript[#DCSScript+1] = string.format("local mystatic  = STATIC:FindByName(\"%s\") ", warehouse)       -- The static that holds the warehouse self object.
  DCSScript[#DCSScript+1] = string.format('local warehouse = mystatic:GetState(mystatic, \"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

--- Get the proper terminal type based on generalized attribute of the group.
--@param #WAREHOUSE self
--@param #WAREHOUSE.Attribute _attribute Generlized attibute of unit.
--@return Wrapper.Airbase#AIRBASE.TerminalType Terminal type for this group.
function WAREHOUSE:_GetTerminal(_attribute)

  -- Default terminal is "large".
  local _terminal=AIRBASE.TerminalType.OpenBig
  
  
  if _attribute==WAREHOUSE.Attribute.AIR_FIGHTER 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
  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=100
  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 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!
  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: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 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
    
    --[[
    -- TODO Clients? Unoccupied client aircraft are also important! Are they already included in scanned units maybe?
    local clients=_DATABASE.CLIENTS
    for _,_client in pairs(clients) do
      local client=_client --Wrapper.Client#CLIENT
      env.info(string.format("FF Client name %s", client:GetName()))
      local unit=UNIT:FindByName(client:GetName())
      --local unit=client:GetClientGroupUnit()      
      local _coord=unit:GetCoordinate()
      local _name=unit:GetName()
      local _size=self:_GetObjectSize(client:GetClientGroupDCSUnit())
      table.insert(obstacles,{coord=_coord, size=_size, name=_name, type="client"})
    end 
    ]]    
  end
  
  -- 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
    
    -- Get terminal type of this asset
    local terminaltype=self:_GetTerminal(asset.attribute)
    
    -- Asset specific parking.
    parking[_asset.uid]={}
    
    -- Loop over all units - each one needs a spot.
    for i=1,_asset.nunits do
  
      -- 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          
  
          -- Coordinate of the parking spot.
          local _spot=parkingspot.Coordinate   -- Core.Point#COORDINATE
          local _termid=parkingspot.TerminalID
          local _toac=parkingspot.TOAC
          
          --env.info(string.format("FF asset=%s (id=%d): needs terminal type=%d, id=%d, #obstacles=%d", _asset.templatename, _asset.uid, terminaltype, _termid, #obstacles))
           
          -- Loop over all obstacles.
          local free=true
          local problem=nil
          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
              --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", _asset.templatename, _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", _asset.templatename, _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)
            
            self:T(self.wid..string.format("Parking spot #%d is free for asset id=%d!", _termid, _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=_asset.templatename, type="asset"})
            
            gotit=true
            break
            
          else
          
            -- Debug output for occupied spots.
            self:T(self.wid..string.format("Parking spot #%d is occupied or not big enough!", _termid))
            if self.Debug then
              local coord=problem.coord --Core.Point#COORDINATE
              local text=string.format("Obstacle blocking spot #%d is %s type %s with size=%.1f m and distance=%.1f m.", _termid, problem.name, problem.type, problem.size, problem.dist)
              coord:MarkToAll(string.format(text))
            end
            
          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.wid..string.format("WARNING: No free parking spot for asset id=%d",_asset.uid))
        return nil
      end      
    end -- loop over asset units
  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)

  -- 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    
  
  return nil
end


--- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived. 
-- @param #WAREHOUSE self
-- @param #WAREHOUSE.Assetitem _assetitem Asset for which the name is created.
-- @param #WAREHOUSE.Queueitem _queueitem (Optional) Request specific name.
-- @return #string Alias name "UnitType\_WID-%d\_AID-%d\_RID-%d"
function WAREHOUSE:_Alias(_assetitem,_queueitem)
  return self:_alias(_assetitem.unittype, self.uid, _assetitem.uid,_queueitem.uid)
end

--- Creates a unique name for spawned assets. From the group name the original warehouse, global asset and the request can be derived.
-- @param #WAREHOUSE self
-- @param #string unittype Type of unit.
-- @param #number wid Warehouse id.
-- @param #number aid Asset item id.
-- @param #number qid Queue/request item id.
-- @return #string Alias name "UnitType\_WID-%d\_AID-%d\_RID-%d"
function WAREHOUSE:_alias(unittype, wid, aid, qid)
  local _alias=string.format("%s_WID-%d_AID-%d", unittype, wid, aid)
  if qid then
    _alias=_alias..string.format("_RID-%d", qid)
  end
  return _alias
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)
  if group then
    local name
    if type(group)=="string" then
      name=group
    else
      name=group:GetName()
    end
    local namewithout=UTILS.Split(name, "#")[1]
    if namewithout then
      return namewithout
    else
      return name
    end
  end
  if type(group)=="string" then
    return group
  else
    return group:GetName()
  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)

  ---@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
  
  if group then
  
    -- Group name
    local name=group:GetName()
      
    -- Get ids
    local wid,aid,rid=analyse(name)
    
    -- Debug info
    self:T3(self.wid..string.format("Group Name   = %s", tostring(name)))  
    self:T3(self.wid..string.format("Warehouse ID = %s", tostring(wid)))
    self:T3(self.wid..string.format("Asset     ID = %s", tostring(aid)))
    self:T3(self.wid..string.format("Request   ID = %s", tostring(rid)))
    
    return wid,aid,rid
  else
    self:E("WARNING: Group not found in GetIDsFromGroup() function!")
  end

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={}

  -- 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
    
    -----------
    --- Air ---
    -----------   
    -- 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")

    --------------
    --- Ground ---
    --------------    
    -- Ground
    local apc=group:HasAttribute("Infantry carriers")
    local truck=group:HasAttribute("Trucks") and group:GetCategory()==Group.Category.GROUND
    local infantry=group:HasAttribute("Infantry")
    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")
    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 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)
  self:F({qitem=qitem, queue=queue})
  
  for i=1,#queue do
    local _item=queue[i] --#WAREHOUSE.Queueitem
    if _item.uid==qitem.uid then
      self:T(self.wid..string.format("Deleting queue item id=%d.", qitem.uid))
      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

--- 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)

  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
    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
  
  self:I(self.wid..text)
end

--- Display status of warehouse.
-- @param #WAREHOUSE self
function WAREHOUSE:_DisplayStatus()  
  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:T(text)
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 coaliton of the warehouse owner is able to see it.
-- @param #WAREHOUSE self
-- @return #string Text about warehouse stock
function WAREHOUSE:_UpdateWarehouseMarkText()

  -- Create a mark with the current assets in stock.
  if self.markerid~=nil then
    trigger.action.removeMark(self.markerid)
  end
  
  -- Get assets in stock.
  local _data=self:GetStockInfo(self.stock)

  -- Text.  
  local text=string.format("Warehouse state: %s\nTotal assets in stock %d:\n", self:GetState(), #self.stock)

  for _attribute,_count in pairs(_data) do
    if _count>0 then
      local attribute=tostring(UTILS.Split(_attribute, "_")[2])
      text=text..string.format("%s=%d, ", attribute,_count)
    end
  end
  
  -- Create/update marker at warehouse in F10 map.
  self.markerid=self:GetCoordinate():MarkToCoalition(text, self:GetCoalition(), true)
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.wid..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 then
    MESSAGE:New(text, duration):ToCoalitionIf(self:GetCoalition(), self.Debug or self.Report)
  end
  self:I(self.wid..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 duration>0 then
    MESSAGE:New(text, duration):ToAllIf(self.Debug)
  end
  self:T(self.wid..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.wid..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.wid..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={}
  
  --- Departure/Take-off
  c[#c+1]=Pdeparture
  wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, 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, 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, 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, true, nil, nil, "Holding")  

  --- Final destination.  
  c[#c+1]=Pdestination
  wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, 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


-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

--[[
  --- Departure/Take-off
  c[#c+1]=Pdeparture
  wp[#wp+1]=Pdeparture:WaypointAir("RADIO", COORDINATE.WaypointType.TakeOffParking, COORDINATE.WaypointAction.FromParkingArea, VxClimb, true, departure, nil, "Departure")
  
  --- Climb 
  local Pclimb=Pdeparture:Translate(d_climb/2, heading)
  Pclimb.y=H_departure+(FLcruise-H_departure)/2
  c[#c+1]=Pclimb
  wp[#wp+1]=Pclimb:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxClimb, true, nil, nil, "Climb")
  
  --- Begin of Cruise
  local Pcruise1=Pclimb:Translate(d_climb/2, heading)
  Pcruise1.y=FLcruise
  c[#c+1]=Pcruise1
  wp[#wp+1]=Pcruise1:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "Begin of Cruise")

  --- End of Cruise    
  local Pcruise2=Pcruise1:Translate(d_cruise, heading)
  Pcruise2.y=FLcruise
  c[#c+1]=Pcruise2
  wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxCruise, true, nil, nil, "End of Cruise")

  --- Descent  
  local Pdescent=Pcruise2:Translate(d_descent/2, heading)
  Pdescent.y=FLcruise-(FLcruise-(h_holding+H_holding))/2
  c[#c+1]=Pdescent
  wp[#wp+1]=Pcruise2:WaypointAir("BARO", COORDINATE.WaypointType.TurningPoint, COORDINATE.WaypointAction.TurningPoint, VxDescent, 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, true, nil, nil, "Holding")  

  --- Final destination.  
  c[#c+1]=Pdestination
  wp[#wp+1]=Pdestination:WaypointAir("RADIO", COORDINATE.WaypointType.Land, COORDINATE.WaypointAction.Landing, VxFinal, true,  destination, nil, "Final Destination")
]]
--- **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-release/AIB%20-%20AI%20Balancing)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl2CJVIrL1TdAumuVS8n64B7)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: 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


--- 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 @{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.
-- 
-- @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

--- @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

--- @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

--- @param #AI_BALANCER self
-- @param Core.Set#SET_GROUP SetGroup
-- @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.
      --TODO: i need to rework the POINT_VEC2 thing.
      local PointVec2 = POINT_VEC2:New( AIGroup:GetVec2().x, AIGroup:GetVec2().y  )
      local ClosestAirbase = self.ReturnAirbaseSet:FindNearestAirbaseFromPointVec2( PointVec2 )
      self:T( ClosestAirbase.AirbaseName )
      AIGroup:MessageToRed( "Returning to " .. ClosestAirbase:GetName().. " ...", 30 )
      local RTBRoute = AIGroup:RouteReturnToAirbase( ClosestAirbase )
      AIGroupTemplate.route = RTBRoute
      AIGroup:Respawn( AIGroupTemplate )
    end

end


--- @param #AI_BALANCER self
function AI_BALANCER:onenterMonitoring( SetGroup )

  self:T2( { self.SetClient:Count() } )
  --self.SetClient:Flush()

  self.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(
              --- @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,
              
              --- @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** -- (R2.2) - Models the process of air operations for airplanes.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===
-- 
-- @module AI.AI_A2A
-- @image AI_Air_To_Air_Dispatching.JPG

--BASE:TraceClass("AI_A2A")


--- @type AI_A2A
-- @extends Core.Fsm#FSM_CONTROLLABLE

--- The AI_A2A class implements the core functions to operate an AI @{Wrapper.Group} A2A tasking.
-- 
-- 
-- ## AI_A2A constructor
--   
--   * @{#AI_A2A.New}(): Creates a new AI_A2A object.
-- 
-- ## 2. AI_A2A is a FSM
-- 
-- ![Process](..\Presentations\AI_PATROL\Dia2.JPG)
-- 
-- ### 2.1. AI_A2A 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 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 tresholds have been reached, the AI will RTB.
--    
-- ## 3. Set or Get the AI controllable
-- 
--   * @{#AI_A2A.SetControllable}(): Set the AIControllable.
--   * @{#AI_A2A.GetControllable}(): Get the AIControllable.
--
-- @field #AI_A2A
AI_A2A = {
  ClassName = "AI_A2A",
}

--- Creates a new AI_A2A object
-- @param #AI_A2A self
-- @param Wrapper.Group#GROUP AIGroup The GROUP object to receive the A2A Process.
-- @return #AI_A2A
function AI_A2A:New( AIGroup )

  -- Inherits from BASE
  local self = BASE:Inherit( self, FSM_CONTROLLABLE:New() ) -- #AI_A2A
  
  self:SetControllable( AIGroup )
  
  self:SetFuelThreshold( .2, 60 )
  self:SetDamageThreshold( 0.4 )
  self:SetDisengageRadius( 70000 )

  self:SetStartState( "Stopped" ) 
  
  self:AddTransition( "*", "Start", "Started" )
  
  --- Start Handler OnBefore for AI_A2A
  -- @function [parent=#AI_A2A] OnBeforeStart
  -- @param #AI_A2A self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @return #boolean
  
  --- Start Handler OnAfter for AI_A2A
  -- @function [parent=#AI_A2A] OnAfterStart
  -- @param #AI_A2A self
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  
  --- Start Trigger for AI_A2A
  -- @function [parent=#AI_A2A] Start
  -- @param #AI_A2A self
  
  --- Start Asynchronous Trigger for AI_A2A
  -- @function [parent=#AI_A2A] __Start
  -- @param #AI_A2A self
  -- @param #number Delay

  self:AddTransition( "*", "Stop", "Stopped" )

--- OnLeave Transition Handler for State Stopped.
-- @function [parent=#AI_A2A] OnLeaveStopped
-- @param #AI_A2A 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_A2A] OnEnterStopped
-- @param #AI_A2A 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_A2A] OnBeforeStop
-- @param #AI_A2A 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_A2A] OnAfterStop
-- @param #AI_A2A 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_A2A] Stop
-- @param #AI_A2A self

--- Asynchronous Event Trigger for Event Stop.
-- @function [parent=#AI_A2A] __Stop
-- @param #AI_A2A self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Status", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A.

--- OnBefore Transition Handler for Event Status.
-- @function [parent=#AI_A2A] OnBeforeStatus
-- @param #AI_A2A 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_A2A] OnAfterStatus
-- @param #AI_A2A 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_A2A] Status
-- @param #AI_A2A self

--- Asynchronous Event Trigger for Event Status.
-- @function [parent=#AI_A2A] __Status
-- @param #AI_A2A self
-- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "RTB", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A.

--- OnBefore Transition Handler for Event RTB.
-- @function [parent=#AI_A2A] OnBeforeRTB
-- @param #AI_A2A 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_A2A] OnAfterRTB
-- @param #AI_A2A 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_A2A] RTB
-- @param #AI_A2A self

--- Asynchronous Event Trigger for Event RTB.
-- @function [parent=#AI_A2A] __RTB
-- @param #AI_A2A self
-- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Returning.
-- @function [parent=#AI_A2A] OnLeaveReturning
-- @param #AI_A2A 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_A2A] OnEnterReturning
-- @param #AI_A2A 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_A2A
  -- @function [parent=#AI_A2A] OnBeforeRefuel
  -- @param #AI_A2A 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_A2A
  -- @function [parent=#AI_A2A] OnAfterRefuel
  -- @param #AI_A2A 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_A2A
  -- @function [parent=#AI_A2A] Refuel
  -- @param #AI_A2A self
  
  --- Refuel Asynchronous Trigger for AI_A2A
  -- @function [parent=#AI_A2A] __Refuel
  -- @param #AI_A2A 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
  
  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_A2A:SetDispatcher( Dispatcher )
  self.Dispatcher = Dispatcher
end

function AI_A2A:GetDispatcher()
  return self.Dispatcher
end

function AI_A2A: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_A2A:ClearTargetDistance()

  self.TargetDistance = nil
  self.ClosestTargetDistance = nil
end


--- Sets (modifies) the minimum and maximum speed of the patrol.
-- @param #AI_A2A 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_A2A self
function AI_A2A: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 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 self
function AI_A2A:SetAltitude( PatrolFloorAltitude, PatrolCeilingAltitude )
  self:F2( { PatrolFloorAltitude, PatrolCeilingAltitude } )
  
  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
end


--- Sets the home airbase.
-- @param #AI_A2A self
-- @param Wrapper.Airbase#AIRBASE HomeAirbase
-- @return #AI_A2A self
function AI_A2A:SetHomeAirbase( HomeAirbase )
  self:F2( { HomeAirbase } )
  
  self.HomeAirbase = HomeAirbase
end

--- Sets to refuel at the given tanker.
-- @param #AI_A2A self
-- @param Wrapper.Group#GROUP TankerName The group name of the tanker as defined within the Mission Editor or spawned.
-- @return #AI_A2A self
function AI_A2A: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_A2A self
-- @param #number DisengageRadius The disengage range.
-- @return #AI_A2A self
function AI_A2A:SetDisengageRadius( DisengageRadius )
  self:F2( { DisengageRadius } )
  
  self.DisengageRadius = DisengageRadius
end

--- Set the status checking off.
-- @param #AI_A2A self
-- @return #AI_A2A self
function AI_A2A: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 treshold is calculated.
-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted to the AI_A2A.
-- Once the time is finished, the old AI will return to the base.
-- @param #AI_A2A self
-- @param #number PatrolFuelThresholdPercentage The treshold 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_A2A self
function AI_A2A:SetFuelThreshold( PatrolFuelThresholdPercentage, PatrolOutOfFuelOrbitTime )

  self.PatrolFuelThresholdPercentage = PatrolFuelThresholdPercentage
  self.PatrolOutOfFuelOrbitTime = PatrolOutOfFuelOrbitTime
  
  self.Controllable:OptionRTBBingoFuel( false )
  
  return self
end

--- When the AI is damaged beyond a certain treshold, it is required that the AI returns to the home base.
-- However, damage cannot be foreseen early on. 
-- Therefore, when the damage treshold 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 treshold will be 0.25.
-- @param #AI_A2A self
-- @param #number PatrolDamageThreshold The treshold in percentage (between 0 and 1) when the AI is considered to be damaged.
-- @return #AI_A2A self
function AI_A2A:SetDamageThreshold( PatrolDamageThreshold )

  self.PatrolManageDamage = true
  self.PatrolDamageThreshold = PatrolDamageThreshold
  
  return self
end

--- Defines a new patrol route using the @{Process_PatrolZone} parameters and settings.
-- @param #AI_A2A self
-- @return #AI_A2A 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_A2A: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



--- @param #AI_A2A self
function AI_A2A:onbeforeStatus()

  return self.CheckStatus
end

--- @param #AI_A2A self
function AI_A2A: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() )
      self:F({DistanceFromHomeBase=DistanceFromHomeBase})
      
      if DistanceFromHomeBase > self.DisengageRadius then
        self:E( 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" ) then
      local Fuel = self.Controllable:GetFuelMin()
      self:F({Fuel=Fuel, PatrolFuelThresholdPercentage=self.PatrolFuelThresholdPercentage})
      if Fuel < self.PatrolFuelThresholdPercentage then
        if self.TankerName then
          self:E( self.Controllable:GetName() .. " is out of fuel: " .. Fuel .. " ... Refuelling at Tanker!" )
          self:Refuel()
        else
          self:E( 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 )
    
          self:Fuel()
          RTB = true
        end
      else
      end
    end
    
    -- TODO: Check GROUP damage function.
    local Damage = self.Controllable:GetLife()
    local InitialLife = self.Controllable:GetLife0()
    self:F( { Damage = Damage, InitialLife = InitialLife, DamageThreshold = self.PatrolDamageThreshold } )
    if ( Damage / InitialLife ) < self.PatrolDamageThreshold then
      self:E( 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 >= 2 then
          if Damage ~= InitialLife then
            self:Damaged()
          else  
            self:E( 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( 0.5 )
    end

    if not self:Is("Home") then
      self:__Status( 10 )
    end
    
  end
end


--- @param Wrapper.Group#GROUP AIGroup
function AI_A2A.RTBRoute( AIGroup, Fsm )

  AIGroup:F( { "AI_A2A.RTBRoute:", AIGroup:GetName() } )
  
  if AIGroup:IsAlive() then
    Fsm:__RTB( 0.5 )
  end
  
end

--- @param Wrapper.Group#GROUP AIGroup
function AI_A2A.RTBHold( AIGroup, Fsm )

  AIGroup:F( { "AI_A2A.RTBHold:", AIGroup:GetName() } )
  if AIGroup:IsAlive() then
    Fsm:__RTB( 0.5 )
    Fsm:Return()
    local Task = AIGroup:TaskOrbitCircle( 4000, 400 )
    AIGroup:SetTask( Task )
  end
  
end


--- @param #AI_A2A self
-- @param Wrapper.Group#GROUP AIGroup
function AI_A2A:onafterRTB( AIGroup, From, Event, To )
  self:F( { AIGroup, From, Event, To } )

  
  if AIGroup and AIGroup:IsAlive() then

    self:E( "Group " .. AIGroup:GetName() .. " ... RTB! ( " .. self:GetState() .. " )" )
    
    self:ClearTargetDistance()
    AIGroup:ClearTasks()

    local EngageRoute = {}

    --- Calculate the target route point.
    
    local CurrentCoord = AIGroup:GetCoordinate()
    local ToTargetCoord = self.HomeAirbase:GetCoordinate()
    local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
    local ToAirbaseAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) )

    local Distance = CurrentCoord:Get2DDistance( ToTargetCoord )
    
    local ToAirbaseCoord = CurrentCoord:Translate( 5000, ToAirbaseAngle )
    if Distance < 5000 then
      self:E( "RTB and near the airbase!" )
      self:Home()
      return
    end
    --- Create a route point of type air.
    local ToRTBRoutePoint = ToAirbaseCoord:WaypointAir( 
      self.PatrolAltType, 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.TurningPoint, 
      ToTargetSpeed, 
      true 
    )

    self:F( { Angle = ToAirbaseAngle, ToTargetSpeed = ToTargetSpeed } )
    self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } )
    
    EngageRoute[#EngageRoute+1] = ToRTBRoutePoint
    EngageRoute[#EngageRoute+1] = ToRTBRoutePoint
    
    AIGroup:OptionROEHoldFire()
    AIGroup:OptionROTEvadeFire()

    --- Now we're going to do something special, we're going to call a function from a waypoint action at the AIControllable...
    AIGroup:WayPointInitialize( EngageRoute )
  
    local Tasks = {}
    Tasks[#Tasks+1] = AIGroup:TaskFunction( "AI_A2A.RTBRoute", self )
    EngageRoute[#EngageRoute].task = AIGroup:TaskCombo( Tasks )

    --- NOW ROUTE THE GROUP!
    AIGroup:Route( EngageRoute, 0.5 )
      
  end
    
end

--- @param #AI_A2A self
-- @param Wrapper.Group#GROUP AIGroup
function AI_A2A:onafterHome( AIGroup, From, Event, To )
  self:F( { AIGroup, From, Event, To } )

  self:E( "Group " .. self.Controllable:GetName() .. " ... Home! ( " .. self:GetState() .. " )" )
  
  if AIGroup and AIGroup:IsAlive() then
  end

end



--- @param #AI_A2A self
-- @param Wrapper.Group#GROUP AIGroup
function AI_A2A:onafterHold( AIGroup, From, Event, To, HoldTime )
  self:F( { AIGroup, From, Event, To } )

  self:E( "Group " .. self.Controllable:GetName() .. " ... Holding! ( " .. self:GetState() .. " )" )
  
  if AIGroup and AIGroup:IsAlive() then
    local OrbitTask = AIGroup:TaskOrbitCircle( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ), self.PatrolMinSpeed )
    local TimedOrbitTask = AIGroup:TaskControlled( OrbitTask, AIGroup:TaskCondition( nil, nil, nil, nil, HoldTime , nil ) )
    
    local RTBTask = AIGroup:TaskFunction( "AI_A2A.RTBHold", self )
    
    local OrbitHoldTask = AIGroup:TaskOrbitCircle( 4000, self.PatrolMinSpeed )
    
    --AIGroup:SetState( AIGroup, "AI_A2A", self )
    
    AIGroup:SetTask( AIGroup:TaskCombo( { TimedOrbitTask, RTBTask, OrbitHoldTask } ), 1 )
  end

end

--- @param Wrapper.Group#GROUP AIGroup
function AI_A2A.Resume( AIGroup, Fsm )

  AIGroup:I( { "AI_A2A.Resume:", AIGroup:GetName() } )
  if AIGroup:IsAlive() then
    Fsm:__RTB( 0.5 )
  end
  
end

--- @param #AI_A2A self
-- @param Wrapper.Group#GROUP AIGroup
function AI_A2A:onafterRefuel( AIGroup, From, Event, To )
  self:F( { AIGroup, From, Event, To } )

  self:E( "Group " .. self.Controllable:GetName() .. " ... Refuelling! ( " .. self:GetState() .. " )" )
  
  if AIGroup and AIGroup:IsAlive() then
    local Tanker = GROUP:FindByName( self.TankerName )
    if Tanker:IsAlive() and Tanker:IsAirPlane() then

      local RefuelRoute = {}
  
      --- Calculate the target route point.
      
      local CurrentCoord = AIGroup:GetCoordinate()
      local ToRefuelCoord = Tanker:GetCoordinate()
      local ToRefuelSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
      
      --- Create a route point of type air.
      local ToRefuelRoutePoint = ToRefuelCoord:WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.TurningPoint, 
        ToRefuelSpeed, 
        true 
      )
  
      self:F( { ToRefuelSpeed = ToRefuelSpeed } )
      
      RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint
      RefuelRoute[#RefuelRoute+1] = ToRefuelRoutePoint
      
      AIGroup:OptionROEHoldFire()
      AIGroup:OptionROTEvadeFire()
  
      local Tasks = {}
      Tasks[#Tasks+1] = AIGroup:TaskRefueling()
      Tasks[#Tasks+1] = AIGroup:TaskFunction( self:GetClassName() .. ".Resume", self )
      RefuelRoute[#RefuelRoute].task = AIGroup:TaskCombo( Tasks )
  
      AIGroup:Route( RefuelRoute, 0.5 )
    else
      self:RTB()
    end
  end

end
    


--- @param #AI_A2A self
function AI_A2A:onafterDead()
  self:SetStatusOff()
end


--- @param #AI_A2A self
-- @param Core.Event#EVENTDATA EventData
function AI_A2A:OnCrash( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:E( self.Controllable:GetUnits() )
    if #self.Controllable:GetUnits() == 1 then
      self:__Crash( 1, EventData )
    end
  end
end

--- @param #AI_A2A self
-- @param Core.Event#EVENTDATA EventData
function AI_A2A:OnEjection( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:__Eject( 1, EventData )
  end
end

--- @param #AI_A2A self
-- @param Core.Event#EVENTDATA EventData
function AI_A2A:OnPilotDead( EventData )

  if self.Controllable:IsAlive() and EventData.IniDCSGroupName == self.Controllable:GetName() then
    self:__PilotDead( 1, EventData )
  end
end
--- **AI** -- (R2.2) - 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 @{Zone} by an AI @{Wrapper.Group} or @{Wrapper.Group}.
-- 
-- ![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 treshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel treshold 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 tresholds 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 treshold is calculated.
-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, 
-- while a new AI is targetted 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 treshold 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.
-- 
-- ===
-- 
-- @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
-- @param Core.Zone#ZONE_BASE PatrolZone The @{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_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 )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_A2A:New( AIPatrol ) ) -- #AI_A2A_PATROL
  
  self.PatrolZone = PatrolZone
  self.PatrolFloorAltitude = PatrolFloorAltitude
  self.PatrolCeilingAltitude = PatrolCeilingAltitude
  self.PatrolMinSpeed = PatrolMinSpeed
  self.PatrolMaxSpeed = PatrolMaxSpeed
  
  -- 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_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 @{Process_PatrolZone} 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



--- @param Wrapper.Group#GROUP AIPatrol
-- This statis method is called from the route path within the last task at the last waaypoint of the AIPatrol.
-- Note that this method is required, as triggers the next route when patrolling for the AIPatrol.
function AI_A2A_PATROL.PatrolRoute( AIPatrol, Fsm )

  AIPatrol:F( { "AI_A2A_PATROL.PatrolRoute:", AIPatrol:GetName() } )

  if AIPatrol:IsAlive() then
    Fsm:Route()
  end
  
end


--- Defines a new patrol route using the @{Process_PatrolZone} 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:IsAlive() then
    
    local PatrolRoute = {}

    --- Calculate the target route point.
    
    local CurrentCoord = AIPatrol:GetCoordinate()
    
    local ToTargetCoord = self.PatrolZone:GetRandomPointVec2()
    ToTargetCoord:SetAlt( math.random( self.PatrolFloorAltitude, self.PatrolCeilingAltitude ) )
    self:SetTargetDistance( ToTargetCoord ) -- For RTB status check
    
    local ToTargetSpeed = math.random( self.PatrolMinSpeed, self.PatrolMaxSpeed )
    
    --- Create a route point of type air.
    local ToPatrolRoutePoint = ToTargetCoord:WaypointAir( 
      self.PatrolAltType, 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.TurningPoint, 
      ToTargetSpeed, 
      true 
    )

    PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint
    PatrolRoute[#PatrolRoute+1] = ToPatrolRoutePoint
    
    local Tasks = {}
    Tasks[#Tasks+1] = AIPatrol:TaskFunction( "AI_A2A_PATROL.PatrolRoute", self )
    PatrolRoute[#PatrolRoute].task = AIPatrol:TaskCombo( Tasks )
    
    AIPatrol:OptionROEReturnFire()
    AIPatrol:OptionROTEvadeFire()

    AIPatrol:Route( PatrolRoute, 0.5 )
  end

end

--- @param Wrapper.Group#GROUP AIPatrol
function AI_A2A_PATROL.Resume( AIPatrol, Fsm )

  AIPatrol:I( { "AI_A2A_PATROL.Resume:", AIPatrol:GetName() } )
  if AIPatrol:IsAlive() then
    Fsm:__Reset( 1 )
    Fsm:__Route( 5 )
  end
  
end
--- **AI** -- (R2.2) - Models the process of Combat Air Patrol (CAP) for airplanes.
--
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ===       
--
-- @module AI.AI_A2A_Cap
-- @image AI_Combat_Air_Patrol.JPG

--- @type AI_A2A_CAP
-- @extends AI.AI_A2A_Patrol#AI_A2A_PATROL


--- The AI_A2A_CAP class implements the core functions to patrol a @{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.
-- 
-- ![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 treshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel treshold 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 tresholds 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.AI_CAP#AI_A2A_CAP.SetEngageRange}() to define that range.
--
-- ## 4. Set the Zone of Engagement
-- 
-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG)
-- 
-- An optional @{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_A2A_CAP.SetEngageZone}() to define that Zone.
--  
-- ===
-- 
-- @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 Core.Zone#ZONE_BASE PatrolZone The @{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 )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_A2A_PATROL:New( AICap, PatrolZone, PatrolFloorAltitude, PatrolCeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, PatrolAltType ) ) -- #AI_A2A_CAP

  self.Accomplished = false
  self.Engaging = false
  
  self.EngageMinSpeed = EngageMinSpeed
  self.EngageMaxSpeed = EngageMaxSpeed
  
  self:AddTransition( { "Patrolling", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP.

  --- OnBefore Transition Handler for Event Engage.
  -- @function [parent=#AI_A2A_CAP] OnBeforeEngage
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterEngage
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Engage
  -- @param #AI_A2A_CAP self
  
  --- Asynchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_A2A_CAP] __Engage
  -- @param #AI_A2A_CAP self
  -- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_A2A_CAP] OnLeaveEngaging
-- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnEnterEngaging
-- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap 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_A2A_CAP.
  
  --- OnBefore Transition Handler for Event Fired.
  -- @function [parent=#AI_A2A_CAP] OnBeforeFired
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterFired
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Fired
  -- @param #AI_A2A_CAP self
  
  --- Asynchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_A2A_CAP] __Fired
  -- @param #AI_A2A_CAP self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP.

  --- OnBefore Transition Handler for Event Destroy.
  -- @function [parent=#AI_A2A_CAP] OnBeforeDestroy
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterDestroy
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Destroy
  -- @param #AI_A2A_CAP self
  
  --- Asynchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_A2A_CAP] __Destroy
  -- @param #AI_A2A_CAP self
  -- @param #number Delay The delay in seconds.


  self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP.

  --- OnBefore Transition Handler for Event Abort.
  -- @function [parent=#AI_A2A_CAP] OnBeforeAbort
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterAbort
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Abort
  -- @param #AI_A2A_CAP self
  
  --- Asynchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_A2A_CAP] __Abort
  -- @param #AI_A2A_CAP self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_CAP.

  --- OnBefore Transition Handler for Event Accomplish.
  -- @function [parent=#AI_A2A_CAP] OnBeforeAccomplish
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] OnAfterAccomplish
  -- @param #AI_A2A_CAP self
  -- @param Wrapper.Group#GROUP AICap 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_A2A_CAP] Accomplish
  -- @param #AI_A2A_CAP self
  
  --- Asynchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_A2A_CAP] __Accomplish
  -- @param #AI_A2A_CAP self
  -- @param #number Delay The delay in seconds.  

  return self
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 ).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

--- 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:onafterPatrol( AICap, From, Event, To )

  -- Call the parent Start event handler
  self:GetParent(self).onafterPatrol( self, AICap, From, Event, To )
  self:HandleEvent( EVENTS.Dead )

end

-- todo: need to fix this global function

--- @param Wrapper.Group#GROUP AICap
function AI_A2A_CAP.AttackRoute( AICap, Fsm )

  AICap:F( { "AI_A2A_CAP.AttackRoute:", AICap:GetName() } )

  if AICap:IsAlive() then
    Fsm:__Engage( 0.5 )
  end
end

--- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap 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_CAP:onbeforeEngage( AICap, From, Event, To )
  
  if self.Accomplished == true then
    return false
  end
end

--- @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:onafterAbort( AICap, From, Event, To )
  AICap:ClearTasks()
  self:__Route( 0.5 )
end


--- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap The AICap 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_CAP:onafterEngage( AICap, From, Event, To, AttackSetUnit )

  self:F( { AICap, From, Event, To, AttackSetUnit} )

  self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT
  
  local FirstAttackUnit = self.AttackSetUnit:GetFirst() -- Wrapper.Unit#UNIT
  
  if FirstAttackUnit and FirstAttackUnit:IsAlive() then -- If there is no attacker anymore, stop the engagement.
  
    if AICap:IsAlive() then

      local EngageRoute = {}

      --- Calculate the target route point.
      local CurrentCoord = AICap:GetCoordinate()
      local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate()
      local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed )
      local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) )
      
      --- Create a route point of type air.
      local ToPatrolRoutePoint = CurrentCoord:Translate( 5000, ToInterceptAngle ):WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.TurningPoint, 
        ToTargetSpeed, 
        true 
      )
  
      self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } )
      self:T2( { self.MinSpeed, self.MaxSpeed, ToTargetSpeed } )
      
      EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint
      EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint

      local AttackTasks = {}
  
      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
          AttackTasks[#AttackTasks+1] = AICap:TaskAttackUnit( AttackUnit )
        end
      end
  
      if #AttackTasks == 0 then
        self:E("No targets found -> Going back to Patrolling")
        self:__Abort( 0.5 )
      else
        AICap:OptionROEOpenFire()
        AICap:OptionROTEvadeFire()

        AttackTasks[#AttackTasks+1] = AICap:TaskFunction( "AI_A2A_CAP.AttackRoute", self )
        EngageRoute[#EngageRoute].task = AICap:TaskCombo( AttackTasks )
      end
      
      AICap:Route( EngageRoute, 0.5 )
    end
  else
    self:E("No targets found -> Going back to Patrolling")
    self:__Abort( 0.5 )
  end
end

--- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap 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_CAP:onafterAccomplish( AICap, From, Event, To )
  self.Accomplished = true
  self:SetDetectionOff()
end

--- @param #AI_A2A_CAP self
-- @param Wrapper.Group#GROUP AICap 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_A2A_CAP:onafterDestroy( AICap, From, Event, To, EventData )

  if EventData.IniUnit then
    self.AttackUnits[EventData.IniUnit] = nil
  end
end

--- @param #AI_A2A_CAP self
-- @param Core.Event#EVENTDATA EventData
function AI_A2A_CAP:OnEventDead( EventData )
  self:F( { "EventDead", EventData } )

  if EventData.IniDCSUnit then
    if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then
      self:__Destroy( 1, EventData )
    end
  end  
end

--- @param Wrapper.Group#GROUP AICap
function AI_A2A_CAP.Resume( AICap, Fsm )

  AICap:I( { "AI_A2A_CAP.Resume:", AICap:GetName() } )
  if AICap:IsAlive() then
    Fsm:__Reset( 1 )
    Fsm:__Route( 5 )
  end
  
end
--- **AI** -- (R2.2) - Models the process of Ground Controlled Interception (GCI) for airplanes.
--
-- This is a class used in the @{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.
-- 
-- ![Process](..\Presentations\AI_GCI\Dia3.JPG)
-- 
-- 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.
-- 
-- ![Process](..\Presentations\AI_GCI\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_GCI\Dia5.JPG)
-- 
-- This cycle will continue.
-- 
-- ![Process](..\Presentations\AI_GCI\Dia6.JPG)
-- 
-- During the patrol, the AI will detect enemy targets, which are reported through the **Detected** event.
--
-- ![Process](..\Presentations\AI_GCI\Dia9.JPG)
-- 
-- When enemies are detected, the AI will automatically engage the enemy.
-- 
-- ![Process](..\Presentations\AI_GCI\Dia10.JPG)
-- 
-- Until a fuel or damage treshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel treshold has been reached, the airplane will fly towards the nearest friendly airbase and will land.
-- 
-- ![Process](..\Presentations\AI_GCI\Dia13.JPG)
-- 
-- ## 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 tresholds have been reached, the AI will RTB.
--
-- ## 3. Set the Range of Engagement
-- 
-- ![Range](..\Presentations\AI_GCI\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.AI_GCI#AI_A2A_GCI.SetEngageRange}() to define that range.
--
-- ## 4. Set the Zone of Engagement
-- 
-- ![Zone](..\Presentations\AI_GCI\Dia12.JPG)
-- 
-- An optional @{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_A2A_GCI.SetEngageZone}() to define that Zone.
--  
-- ===
-- 
-- @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
-- @return #AI_A2A_GCI
function AI_A2A_GCI:New( AIIntercept, EngageMinSpeed, EngageMaxSpeed )

  -- Inherits from BASE
  local self = BASE:Inherit( self, AI_A2A:New( AIIntercept ) ) -- #AI_A2A_GCI

  self.Accomplished = false
  self.Engaging = false
  
  self.EngageMinSpeed = EngageMinSpeed
  self.EngageMaxSpeed = EngageMaxSpeed
  self.PatrolMinSpeed = EngageMinSpeed
  self.PatrolMaxSpeed = EngageMaxSpeed
  
  self.PatrolAltType = "RADIO"
  
  self:AddTransition( { "Started", "Engaging", "Returning", "Airborne" }, "Engage", "Engaging" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI.

  --- OnBefore Transition Handler for Event Engage.
  -- @function [parent=#AI_A2A_GCI] OnBeforeEngage
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterEngage
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Engage
  -- @param #AI_A2A_GCI self
  
  --- Asynchronous Event Trigger for Event Engage.
  -- @function [parent=#AI_A2A_GCI] __Engage
  -- @param #AI_A2A_GCI self
  -- @param #number Delay The delay in seconds.

--- OnLeave Transition Handler for State Engaging.
-- @function [parent=#AI_A2A_GCI] OnLeaveEngaging
-- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnEnterEngaging
-- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI.
  
  --- OnBefore Transition Handler for Event Fired.
  -- @function [parent=#AI_A2A_GCI] OnBeforeFired
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterFired
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Fired
  -- @param #AI_A2A_GCI self
  
  --- Asynchronous Event Trigger for Event Fired.
  -- @function [parent=#AI_A2A_GCI] __Fired
  -- @param #AI_A2A_GCI self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "*", "Destroy", "*" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI.

  --- OnBefore Transition Handler for Event Destroy.
  -- @function [parent=#AI_A2A_GCI] OnBeforeDestroy
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterDestroy
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Destroy
  -- @param #AI_A2A_GCI self
  
  --- Asynchronous Event Trigger for Event Destroy.
  -- @function [parent=#AI_A2A_GCI] __Destroy
  -- @param #AI_A2A_GCI self
  -- @param #number Delay The delay in seconds.


  self:AddTransition( "Engaging", "Abort", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI.

  --- OnBefore Transition Handler for Event Abort.
  -- @function [parent=#AI_A2A_GCI] OnBeforeAbort
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterAbort
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Abort
  -- @param #AI_A2A_GCI self
  
  --- Asynchronous Event Trigger for Event Abort.
  -- @function [parent=#AI_A2A_GCI] __Abort
  -- @param #AI_A2A_GCI self
  -- @param #number Delay The delay in seconds.

  self:AddTransition( "Engaging", "Accomplish", "Patrolling" ) -- FSM_CONTROLLABLE Transition for type #AI_A2A_GCI.

  --- OnBefore Transition Handler for Event Accomplish.
  -- @function [parent=#AI_A2A_GCI] OnBeforeAccomplish
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] OnAfterAccomplish
  -- @param #AI_A2A_GCI self
  -- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI] Accomplish
  -- @param #AI_A2A_GCI self
  
  --- Asynchronous Event Trigger for Event Accomplish.
  -- @function [parent=#AI_A2A_GCI] __Accomplish
  -- @param #AI_A2A_GCI self
  -- @param #number Delay The delay in seconds.  

  return self
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 ).onafterStart( self, AIIntercept, From, Event, To )
  AIIntercept:HandleEvent( EVENTS.Takeoff, nil, self )

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:onafterEngage( AIIntercept, From, Event, To )

  self:HandleEvent( EVENTS.Dead )

end

-- todo: need to fix this global function

--- @param Wrapper.Group#GROUP AIControllable
function AI_A2A_GCI.InterceptRoute( AIIntercept, Fsm )

  AIIntercept:F( { "AI_A2A_GCI.InterceptRoute:", AIIntercept:GetName() } )
  
  if AIIntercept:IsAlive() then
    Fsm:__Engage( 0.5 )
  
    --local Task = AIIntercept:TaskOrbitCircle( 4000, 400 )
    --AIIntercept:SetTask( Task )
  end
end

--- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept 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_GCI:onbeforeEngage( AIIntercept, From, Event, To )
  
  if self.Accomplished == true then
    return false
  end
end

--- @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:onafterAbort( AIIntercept, From, Event, To )
  AIIntercept:ClearTasks()
  self:Return()
  self:__RTB( 0.5 )
end


--- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept 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.
function AI_A2A_GCI:onafterEngage( AIIntercept, From, Event, To, AttackSetUnit )

  self:F( { AIIntercept, From, Event, To, AttackSetUnit} )

  self.AttackSetUnit = AttackSetUnit or self.AttackSetUnit -- Core.Set#SET_UNIT
  
  local FirstAttackUnit = self.AttackSetUnit:GetFirst()
  
  if FirstAttackUnit and FirstAttackUnit:IsAlive() then

    if AIIntercept:IsAlive() then
  
      local EngageRoute = {}
      
      local CurrentCoord = AIIntercept:GetCoordinate()
  
      --- Calculate the target route point.
      
      local CurrentCoord = AIIntercept:GetCoordinate()
      
      local ToTargetCoord = self.AttackSetUnit:GetFirst():GetCoordinate()
      self:SetTargetDistance( ToTargetCoord ) -- For RTB status check
      
      local ToTargetSpeed = math.random( self.EngageMinSpeed, self.EngageMaxSpeed )
      local ToInterceptAngle = CurrentCoord:GetAngleDegrees( CurrentCoord:GetDirectionVec3( ToTargetCoord ) )
      
      --- Create a route point of type air.
      local ToPatrolRoutePoint = CurrentCoord:Translate( 15000, ToInterceptAngle ):WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.TurningPoint, 
        ToTargetSpeed, 
        true 
      )
  
      self:F( { Angle = ToInterceptAngle, ToTargetSpeed = ToTargetSpeed } )
      self:F( { self.EngageMinSpeed, self.EngageMaxSpeed, ToTargetSpeed } )
      
      EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint
      EngageRoute[#EngageRoute+1] = ToPatrolRoutePoint
      
      local AttackTasks = {}
  
      for AttackUnitID, AttackUnit in pairs( self.AttackSetUnit:GetSet() ) do
        local AttackUnit = AttackUnit -- Wrapper.Unit#UNIT
        if AttackUnit:IsAlive() and AttackUnit:IsAir() then
          self:T( { "Intercepting Unit:", AttackUnit:GetName(), AttackUnit:IsAlive(), AttackUnit:IsAir() } )
          AttackTasks[#AttackTasks+1] = AIIntercept:TaskAttackUnit( AttackUnit )
        end
      end
        
      if #AttackTasks == 0 then
        self:E("No targets found -> Going RTB")
        self:Return()
        self:__RTB( 0.5 )
      else
        AIIntercept:OptionROEOpenFire()
        AIIntercept:OptionROTEvadeFire()

        AttackTasks[#AttackTasks+1] = AIIntercept:TaskFunction( "AI_A2A_GCI.InterceptRoute", self )
        EngageRoute[#EngageRoute].task = AIIntercept:TaskCombo( AttackTasks )
      end
      
      AIIntercept:Route( EngageRoute, 0.5 )
    
    end
  else
    self:E("No targets found -> Going RTB")
    self:Return()
    self:__RTB( 0.5 )
  end
end

--- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept 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_GCI:onafterAccomplish( AIIntercept, From, Event, To )
  self.Accomplished = true
  self:SetDetectionOff()
end

--- @param #AI_A2A_GCI self
-- @param Wrapper.Group#GROUP AIIntercept 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_A2A_GCI:onafterDestroy( AIIntercept, From, Event, To, EventData )

  if EventData.IniUnit then
    self.AttackUnits[EventData.IniUnit] = nil
  end
end

--- @param #AI_A2A_GCI self
-- @param Core.Event#EVENTDATA EventData
function AI_A2A_GCI:OnEventDead( EventData )
  self:F( { "EventDead", EventData } )

  if EventData.IniDCSUnit then
    if self.AttackUnits and self.AttackUnits[EventData.IniUnit] then
      self:__Destroy( 1, EventData )
    end
  end  
end
--- **AI** - (R2.2) - 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/AID%20-%20AI%20Dispatching/AID-A2A%20-%20AI%20A2A%20Dispatching)
-- 
-- ===
-- 
-- ## YouTube Channel:
-- 
-- [DCS WORLD - MOOSE - A2A GCICAP - Build an automatic A2A Defense System](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0S4KMNUUJpaUs6zZHjLKNx)
-- 
-- ===
-- 
-- # 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 youself 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 @{Detection} classes to perform the EWR detection.
-- Several types of @{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 car 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 @{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 staight 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.**
-- 
-- ===
-- 
-- ### 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 choosen, 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 )
  --    
   -- ### 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/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%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/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%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 @{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/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-009 - AI_A2A - Border Test)
  -- 
  -- 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 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 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 coodination 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 behaviour. 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 treshold.
  -- 
  -- When an airplane gets **out of fuel** to a certain %-tage, 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 treshold** 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 @{Zone} MOOSE classes being used to create zones of different types. Please click the @{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 partrolling. 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" )
  --  
  -- ## 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 alraedy 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 treshold.
  -- 
  -- When an airplane gets **out of fuel** to a certain %-tage, 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 treshold** of spawned airplanes for all squadrons.
  -- 
  -- ## 10.6. Default RTB damage treshold.
  -- 
  -- When an airplane is **damaged** to a certain %-tage, 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 treshold** 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 %-tage 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 treshold 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,
  }


  --- 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
  
  --- Defnes Landing 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 coaliton.
  -- 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 choosen, 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.
    
    -- 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
    
    --- GCI Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] GCI
    -- @param #AI_A2A_DISPATCHER self
    
    --- GCI Asynchronous Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] __GCI
    -- @param #AI_A2A_DISPATCHER self
    -- @param #number Delay
    
    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
    -- @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
    
    --- ENGAGE Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] ENGAGE
    -- @param #AI_A2A_DISPATCHER self
    
    --- ENGAGE Asynchronous Trigger for AI_A2A_DISPATCHER
    -- @function [parent=#AI_A2A_DISPATCHER] __ENGAGE
    -- @param #AI_A2A_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 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


  --- @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
      DefenderSquadron.Resource = {}
      if DefenderSquadron.ResourceCount then
        for Resource = 1, DefenderSquadron.ResourceCount do
          self:ParkDefender( DefenderSquadron )
        end
      end
    end
  end
  

  --- @param #AI_A2A_DISPATCHER self
  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
      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_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:I( "Captured " .. AirbaseName )
    
    -- Now search for all squadrons located at the airbase, and sanatize 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:I( "Squadron " .. SquadronName .. " captured." )
      end
    end
  end

  --- @param #AI_A2A_DISPATCHER self
  -- @param Core.Event#EVENTDATA EventData
  function AI_A2A_DISPATCHER:OnEventCrashOrDead( EventData )
    self.Detection:ForgetDetectedUnit( EventData.IniUnitName ) 
  end

  --- @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, Defender )
        return
      end
      if DefenderUnit:GetLife() ~= DefenderUnit:GetLife0() then
        -- Damaged units cannot be repaired anymore.
        DefenderUnit:Destroy()
        return
      end
    end 
  end
  
  --- @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, Defender )
      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/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-019%20-%20AI_A2A%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 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:SetDisngageRadius() -- 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/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-013%20-%20AI_A2A%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
  -- @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 @{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
  -- @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
  -- @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 treshold when defenders will RTB.
  -- The default damage treshold 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 %-tage of the damage treshold before going RTB.
  -- @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 default damage treshold.
  --   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
  -- @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
  -- @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  


  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 DetectedItem
  -- @return #table A list of the friendlies nearby.
  function AI_A2A_DISPATCHER:GetAIFriendliesNearBy( DetectedItem )
  
    local FriendliesNearBy = self.Detection:GetFriendliesDistance( DetectedItem )
    
    return FriendliesNearBy
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:GetDefenderTasks()
    return self.DefenderTasks or {}
  end
  
  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:GetDefenderTask( Defender )
    return self.DefenderTasks[Defender]
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:GetDefenderTaskFsm( Defender )
    return self:GetDefenderTask( Defender ).Fsm
  end
  
  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:GetDefenderTaskTarget( Defender )
    return self:GetDefenderTask( Defender ).Target
  end
  
  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:GetDefenderTaskSquadronName( Defender )
    return self:GetDefenderTask( Defender ).SquadronName
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_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_A2A_DISPATCHER self
  function AI_A2A_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_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:SetDefenderTask( SquadronName, Defender, Type, Fsm, Target )
  
    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

    if Target then
      self:SetDefenderTaskTarget( Defender, Target )
    end
    return self
  end
  
  
  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param Wrapper.Group#GROUP AIGroup
  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.
  -- 
  -- @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" )
  --   
  --   
  -- @return #AI_A2A_DISPATCHER
  function AI_A2A_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:F( { Squadron = {SquadronName, AirbaseName, TemplatePrefixes, ResourceCount } } )
    
    return self
  end
  
  --- Get an item from the Squadron table.
  -- @param #AI_A2A_DISPATCHER self
  -- @return #table
  function AI_A2A_DISPATCHER:GetSquadron( SquadronName )
    local DefenderSquadron = self.DefenderSquadrons[SquadronName]
    
    if not DefenderSquadron then
      error( "Unknown Squadron:" .. SquadronName )
    end
    
    return DefenderSquadron
  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_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #AI_A2A_DISPATCHER
  -- @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 )
    
    DefenderSquadron.Uncontrolled = true

    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_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #bool 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 )

    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 Core.Zone#ZONE_BASE Zone The @{Zone} object derived from @{Core.Zone#ZONE_BASE} that defines the zone wherein the CAP will be executed.
  -- @param #number FloorAltitude The minimum altitude at which the cap can be executed.
  -- @param #number CeilingAltitude 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, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType )
  
    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.Zone = Zone
    Cap.FloorAltitude = FloorAltitude
    Cap.CeilingAltitude = CeilingAltitude
    Cap.PatrolMinSpeed = PatrolMinSpeed
    Cap.PatrolMaxSpeed = PatrolMaxSpeed
    Cap.EngageMinSpeed = EngageMinSpeed
    Cap.EngageMaxSpeed = EngageMaxSpeed
    Cap.AltType = AltType

    self:SetSquadronCapInterval( SquadronName, self.DefenderDefault.CapLimit, self.DefenderDefault.CapMinSeconds, self.DefenderDefault.CapMaxSeconds, 1 )

    self:F( { CAP = { SquadronName, Zone, FloorAltitude, CeilingAltitude, PatrolMinSpeed, PatrolMaxSpeed, EngageMinSpeed, EngageMaxSpeed, AltType } } )
   
    -- Add the CAP to the EWR network.
    
    local RecceSet = self.Detection:GetDetectionSetGroup()
    RecceSet:FilterPrefixes( DefenderSquadron.TemplatePrefixes )
    RecceSet:FilterStart()
    
    self.Detection:SetFriendlyPrefixes( DefenderSquadron.TemplatePrefixes )
    
    return self
  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
  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

  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @return #table 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


  ---
  -- @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

  
  ---
  -- @param #AI_A2A_DISPATCHER self
  -- @param #string SquadronName The squadron name.
  -- @param #number EngageMinSpeed The minimum speed at which the gci can be executed.
  -- @param #number EngageMaxSpeed The maximum speed at which the gci can be executed.
  -- @usage 
  -- 
  --        -- GCI Squadron execution.
  --        A2ADispatcher:SetSquadronGci( "Mozdok", 900, 1200 )
  --        A2ADispatcher:SetSquadronGci( "Novo", 900, 2100 )
  --        A2ADispatcher:SetSquadronGci( "Maykop", 900, 1200 )
  -- 
  -- @return #AI_A2A_DISPATCHER
  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 %-tage 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_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 )
  -- 
  -- @return #AI_A2A_DISPATCHER
  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 %-tage 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_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 )
  -- 
  -- @return #AI_A2A_DISPATCHER
  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. 
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Set a grouping by default per 2 airplanes.
  --   A2ADispatcher:SetDefaultGrouping( 2 )
  -- 
  -- 
  -- @return #AI_A2A_DISPATCHER
  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. 
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Set a grouping per 2 airplanes.
  --   A2ADispatcher:SetSquadronGrouping( "SquadronName", 2 )
  -- 
  -- 
  -- @return #AI_A2A_DISPATCHER
  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.
  -- @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 )
  -- 
  -- 
  -- @return #AI_A2A_DISPATCHER
  -- 
  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.
  -- @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 )
  -- 
  -- 
  -- @return #AI_A2A_DISPATCHER
  -- 
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default take-off in the air.
  --   A2ADispatcher:SetDefaultTakeoffInAir()
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  function AI_A2A_DISPATCHER:SetDefaultTakeoffInAir()

    self:SetDefaultTakeoff( AI_A2A_DISPATCHER.Takeoff.Air )
    
    return self
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights take-off in the air.
  --   A2ADispatcher:SetSquadronTakeoffInAir( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default take-off from the runway.
  --   A2ADispatcher:SetDefaultTakeoffFromRunway()
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights take-off from the runway.
  --   A2ADispatcher:SetSquadronTakeoffFromRunway( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights by default take-off at a hot parking spot.
  --   A2ADispatcher:SetDefaultTakeoffFromParkingHot()
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights take-off in the air.
  --   A2ADispatcher:SetSquadronTakeoffFromParkingHot( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights take-off from a cold parking spot.
  --   A2ADispatcher:SetDefaultTakeoffFromParkingCold()
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let new flights take-off from a cold parking spot.
  --   A2ADispatcher:SetSquadronTakeoffFromParkingCold( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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.
  -- @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.
  -- 
  -- @return #AI_A2A_DISPATCHER
  -- 
  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.
  -- @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.
  --   
  -- @return #AI_A2A_DISPATCHER
  -- 
  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
  -- @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 )
  -- 
  -- @return #AI_A2A_DISPATCHER
  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
  -- @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 )
  -- 
  -- @return #AI_A2A_DISPATCHER
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let flights by default to land near the airbase and despawn.
  --   A2ADispatcher:SetDefaultLandingNearAirbase()
  --   
  -- @return #AI_A2A_DISPATCHER
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let flights to land near the airbase and despawn.
  --   A2ADispatcher:SetSquadronLandingNearAirbase( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let flights by default land at the runway and despawn.
  --   A2ADispatcher:SetDefaultLandingAtRunway()
  --   
  -- @return #AI_A2A_DISPATCHER
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let flights land at the runway and despawn.
  --   A2ADispatcher:SetSquadronLandingAtRunway( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  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
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let flights by default land and despawn at engine shutdown.
  --   A2ADispatcher:SetDefaultLandingAtEngineShutdown()
  --   
  -- @return #AI_A2A_DISPATCHER
  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.
  -- @usage:
  -- 
  --   local A2ADispatcher = AI_A2A_DISPATCHER:New( ... )
  --   
  --   -- Let flights land and despawn at engine shutdown.
  --   A2ADispatcher:SetSquadronLandingAtEngineShutdown( "SquadronName" )
  --   
  -- @return #AI_A2A_DISPATCHER
  function AI_A2A_DISPATCHER:SetSquadronLandingAtEngineShutdown( SquadronName )

    self:SetSquadronLanding( SquadronName, AI_A2A_DISPATCHER.Landing.AtEngineShutdown )
    
    return self
  end
  
  --- Set the default fuel treshold when defenders will RTB or Refuel in the air.
  -- The fuel treshold 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 %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel.
  -- @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 default fuel treshold.
  --   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 treshold for the squadron when defenders will RTB or Refuel in the air.
  -- The fuel treshold 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 %-tage of the treshold of fuel remaining in the tank when the plane will go RTB or Refuel.
  -- @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 default fuel treshold.
  --   A2ADispatcher:SetSquadronRefuelThreshold( "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
  -- @usage
  -- 
  --   -- Now Setup the A2A dispatcher, and initialize it using the Detection object.
  --   A2ADispatcher = AI_A2A_DISPATCHER:New( Detection )  
  --   
  --   -- Now Setup the default fuel treshold.
  --   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 treshold.
  --   A2ADispatcher:SetSquadronRefuelThreshold( "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  




  --- @param #AI_A2A_DISPATCHER self
  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

  --- @param #AI_A2A_DISPATCHER self
  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
  
  function AI_A2A_DISPATCHER:GetSquadronFromDefender( Defender )
    self.Defenders = self.Defenders or {}
    local DefenderName = Defender:GetName()
    self:F( { DefenderName = DefenderName } )
    return self.Defenders[ DefenderName ] 
  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.
  -- @return #nil If there are no targets to be set.
  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

  ---
  -- @param #AI_A2A_DISPATCHER self
  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: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!
              if DefenderTask.Fsm:Is( "Patrolling" ) or DefenderTask.Fsm:Is( "Engaging" ) or DefenderTask.Fsm:Is( "Refuelling" )
                    or DefenderTask.Fsm:Is( "Started" ) then
                CapCount = CapCount + 1
              end
            end
          end
        end
      end
    end

    return CapCount
  end
  
  
  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:CountDefendersEngaged( AttackerDetection )

    -- First, count the active AIGroups Units, targetting 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
      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
  
  ---
  -- @param #AI_A2A_DISPATCHER self
  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 
        local Friendly = AIFriendly:GetGroup() -- Wrapper.Group#GROUP
        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


  ---
  -- @param #AI_A2A_DISPATCHER self
  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
    
    if self:IsSquadronVisible( SquadronName ) then
    
      -- 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
      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_A2A_DISPATCHER self
  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 Fsm = AI_A2A_CAP:New( DefenderCAP, Cap.Zone, Cap.FloorAltitude, Cap.CeilingAltitude, Cap.PatrolMinSpeed, Cap.PatrolMaxSpeed, Cap.EngageMinSpeed, Cap.EngageMaxSpeed, Cap.AltType )
          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:SetTanker( DefenderSquadron.TankerName or self.DefenderDefault.TankerName )
          Fsm:Start()
  
          self:SetDefenderTask( SquadronName, DefenderCAP, "CAP", Fsm )

          function Fsm:onafterTakeoff( Defender, From, Event, To )
            self:F({"CAP Birth", Defender:GetName()})
            --self:GetParent(self).onafterBirth( self, Defender, From, Event, To )
            
            local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
            local Squadron = Dispatcher:GetSquadronFromDefender( Defender )

            if Squadron then
              Fsm:__Patrol( 2 ) -- Start Patrolling
            end
          end
  
          function Fsm:onafterRTB( Defender, From, Event, To )
            self:F({"CAP RTB", Defender:GetName()})
            self:GetParent(self).onafterRTB( self, Defender, From, Event, To )
            local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
            Dispatcher:ClearDefenderTaskTarget( Defender )
          end
  
          --- @param #AI_A2A_DISPATCHER self
          function Fsm:onafterHome( Defender, From, Event, To, Action )
            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()
              self:ParkDefender( Squadron, Defender )
            end
          end
        end
      end
    end
    
  end


  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:onafterENGAGE( From, Event, To, AttackerDetection, Defenders )
  
    if Defenders then

      for DefenderID, Defender in pairs( Defenders ) do

        local Fsm = self:GetDefenderTaskFsm( Defender )
        Fsm:__Engage( 1, AttackerDetection.Set ) -- Engage on the TargetSetUnit
        
        self:SetDefenderTaskTarget( Defender, AttackerDetection )

      end
    end
  end

  ---
  -- @param #AI_A2A_DISPATCHER self
  function AI_A2A_DISPATCHER:onafterGCI( From, Event, To, AttackerDetection, DefendersMissing, DefenderFriendlies )

    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:__Engage( 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:New( DefenderGCI, Gci.EngageMinSpeed, Gci.EngageMaxSpeed )
                  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( Defender, From, Event, To )
                    self:F({"GCI Birth", Defender:GetName()})
                    --self:GetParent(self).onafterBirth( self, Defender, From, Event, To )
                    
                    local Dispatcher = Fsm:GetDispatcher() -- #AI_A2A_DISPATCHER
                    local Squadron = Dispatcher:GetSquadronFromDefender( Defender )
                    local DefenderTarget = Dispatcher:GetDefenderTaskTarget( Defender )
                    
                    if DefenderTarget then
                      Fsm:__Engage( 2, DefenderTarget.Set ) -- Engage on the TargetSetUnit
                    end
                  end
  
                  function Fsm:onafterRTB( Defender, From, Event, To )
                    self:F({"GCI RTB", Defender:GetName()})
                    self:GetParent(self).onafterRTB( self, Defender, From, Event, To )
                    
                    local Dispatcher = self:GetDispatcher() -- #AI_A2A_DISPATCHER
                    Dispatcher:ClearDefenderTaskTarget( Defender )
                  end
  
                  --- @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
                  
                  --- @param #AI_A2A_DISPATCHER self
                  function Fsm:onafterHome( Defender, From, Event, To, Action )
                    self:F({"GCI 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()
                      self:ParkDefender( Squadron, Defender )
                    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.
  -- @return #nil If there are no targets to be set.
  function AI_A2A_DISPATCHER:EvaluateENGAGE( DetectedItem )
    self:F( { DetectedItem.ItemID } )
  
    -- First, count the active AIGroups Units, targetting 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, 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.
  -- @return #nil If there are no targets to be set.
  function AI_A2A_DISPATCHER:EvaluateGCI( DetectedItem )
    self:F( { DetectedItem.ItemID } )
  
    local AttackerSet = DetectedItem.Set
    local AttackerCount = AttackerSet:Count()

    -- First, count the active AIGroups Units, targetting 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 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( "\nTactical Overview" )

    local DefenderGroupCount = 0

    -- 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( { "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

      if self.TacticalDisplay then      
        -- 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: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

    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 ) )
  
      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_A2A_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_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 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

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
  -- 
  -- ### [AI\_A2A\_GCICAP for Caucasus](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-200%20-%20AI_A2A%20-%20GCICAP%20Demonstration)
  -- ### [AI\_A2A\_GCICAP for NTTR](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-210%20-%20NTTR%20AI_A2A_GCICAP%20Demonstration)
  -- ### [AI\_A2A\_GCICAP for Normandy](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/release-2-2-pre/AID%20-%20AI%20Dispatching/AID-220%20-%20NORMANDY%20AI_A2A_GCICAP%20Demonstration)
  -- 
  -- ### [AI\_A2A\_GCICAP for beta testers](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/AID%20-%20AI%20Dispatching)
  --
  -- ===
  -- 
  -- # 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 choosen 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 choosen 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 taxiiing 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 dissapear 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 coodination 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 partrolling. 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 alraedy 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 simulateneously.  
  -- 
  -- 
  -- ### 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 simulateneously.  
  -- 
  -- @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 defenser 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:I( { Airbases = AirbaseNames  } )

    self:I( "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:I( { 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:I( { 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:I( "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:I( { 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:I( { AirbaseDistance = Distance } )    
          if Distance < AirbaseDistance then
            AirbaseDistance = Distance
            AirbaseClosest = Airbase
          end
        end
      end
      if AirbaseClosest then
        self:I( { 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:I( "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:I( { 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 defenser 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** -- Perform Air Patrolling for airplanes.
-- 
-- **Features:**
-- 
--   * Patrol AI airplanes within a given zone.
--   * Trigger detected events when enemy airplanes are detected.
--   * Manage a fuel treshold 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-release/PAT%20-%20Patrolling)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl35HvYZKA6G22WMt7iI3zky)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **[Dutch_Baron](https://forums.eagle.ru/member.php?u=112075)**: 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](https://forums.eagle.ru/member.php?u=62835)**: 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 @{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 @{Zone} by an AI @{Wrapper.Controllable} or @{Wrapper.Group}.
-- 
-- ![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 treshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel treshold 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 tresholds 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 treshold is calculated.
-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, 
-- while a new AI is targetted 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 proces 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 treshold is reached, the AI will return immediately to the home base (RTB).
-- Use the method @{#AI_PATROL_ZONE.ManageDamage}() to have this proces in place.
-- 
-- ===
-- 
-- @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 @{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 "RADIO" if not specified
  self.PatrolAltType = PatrolAltType or "RADIO"
  
  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 treshold is calculated.
-- When the fuel treshold is reached, the AI will continue for a given time its patrol task in orbit, while a new AIControllable is targetted 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 treshold 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 treshold, it is required that the AI returns to the home base.
-- However, damage cannot be foreseen early on. 
-- Therefore, when the damage treshold 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 treshold will be 0.25.
-- @param #AI_PATROL_ZONE self
-- @param #number PatrolDamageThreshold The treshold 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 @{Process_PatrolZone} parameters and settings.
-- @param #AI_PATROL_ZONE self
-- @return #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: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:E( "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 )
      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

  self:__Detect( -self.DetectInterval )
  
  if Detected == true then
    self:__Detected( 1.5 )
  end
  
end

--- @param Wrapper.Controllable#CONTROLLABLE AIControllable
-- This statis method is called from the route path within the last task at the last waaypoint 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 @{Process_PatrolZone} 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

  
  if self.Controllable:IsAlive() 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:E( "Not in the air, finding route path within PatrolZone" )
      local CurrentVec2 = self.Controllable:GetVec2()
      --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1).
      local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
      local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
      local ToPatrolZoneSpeed = self.PatrolMaxSpeed
      local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
          self.PatrolAltType, 
          POINT_VEC3.RoutePointType.TakeOffParking, 
          POINT_VEC3.RoutePointAction.FromParkingArea, 
          ToPatrolZoneSpeed, 
          true 
        )
      PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint
    else
      self:E( "In the air, finding route path within PatrolZone" )
      local CurrentVec2 = self.Controllable:GetVec2()
      --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1).
      local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
      local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
      local ToPatrolZoneSpeed = self.PatrolMaxSpeed
      local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
          self.PatrolAltType, 
          POINT_VEC3.RoutePointType.TurningPoint, 
          POINT_VEC3.RoutePointAction.TurningPoint, 
          ToPatrolZoneSpeed, 
          true 
        )
      PatrolRoute[#PatrolRoute+1] = CurrentRoutePoint
    end    
    
    
    --- Define a random point in the @{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 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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:E( 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
    else
    end
    
    -- TODO: Check GROUP damage function.
    local Damage = self.Controllable:GetLife()
    if Damage <= self.PatrolDamageThreshold then
      self:E( self.Controllable:GetName() .. " is damaged:" .. Damage .. ", RTB!" )
      RTB = true
    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()
    
    --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
    local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToPatrolZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.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
    self:E( self.Controllable:GetUnits() )
    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 treshold to RTB on time.
--   * Engage the enemy when detected.
-- 
--
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master-release/CAP%20-%20Combat%20Air%20Patrol)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl1YCyPxJgoZn-CfhwyeW65L)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
--
--   * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing.
--   * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing.
--   * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: Test case revision.
--   * **[Whisper](http://forums.eagle.ru/member.php?u=3829): Testing.
--   * **[Delta99](https://forums.eagle.ru/member.php?u=125166): 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 @{Zone} where the patrol needs to be executed.
-- @extends AI.AI_Patrol#AI_PATROL_ZONE


--- Implements the core functions to patrol a @{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.
-- 
-- ![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 treshold has been reached by the AI, or when the AI is commanded to RTB.
-- When the fuel treshold 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 tresholds 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.AI_CAP#AI_CAP_ZONE.SetEngageRange}() to define that range.
--
-- ## 4. Set the Zone of Engagement
-- 
-- ![Zone](..\Presentations\AI_CAP\Dia12.JPG)
-- 
-- An optional @{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_CAP_ZONE.SetEngageZone}() to define that Zone.
--  
-- ===
-- 
-- @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 @{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:IsAlive() then

    local EngageRoute = {}

    --- Calculate the current route point.
    local CurrentVec2 = self.Controllable:GetVec2()
    
    --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
    local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToEngageZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.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 = POINT_VEC3:New( ToTargetVec2.x, ToTargetAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToPatrolRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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-release/CAS%20-%20Close%20Air%20Support)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl3JBO1WDqqpyYRRmIkR2ir2)
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
--
--   * **[Quax](https://forums.eagle.ru/member.php?u=90530)**: Concept, Advice & Testing.
--   * **[Pikey](https://forums.eagle.ru/member.php?u=62835)**: Concept, Advice & Testing.
--   * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: 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 @{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 @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}.
-- 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 treshold 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 treshold 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 tresholds have been reached, the AI will RTB.
-- 
-- ===
-- 
-- @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 @{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()
    
    --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
    local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToEngageZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.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 @{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 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.TurningPoint, 
      self.EngageSpeed, 
      true 
    )
    
    EngageRoute[#EngageRoute+1] = ToTargetRoutePoint
  
    Controllable:Route( EngageRoute, 0.5 )
    
    self:SetRefreshTimeInterval( 2 )
    self:SetDetectionActivated()
    self:__Target( -2 ) -- Start Targetting
  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/BAI%20-%20Battlefield%20Air%20Interdiction)
-- 
-- ===
-- 
-- ### [YouTube Playlist]()
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- ### Contributions: 
-- 
--   * **[Gunterlund](http://forums.eagle.ru:8080/member.php?u=75036)**: 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 @{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 @{Zone} by an AIR @{Wrapper.Controllable} or @{Wrapper.Group}.
-- 
-- 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 treshold 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 treshold 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 tresholds 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.
-- 
-- ===
-- 
-- @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 @{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()
    
    --TODO: Create GetAltitude function for GROUP, and delete GetUnit(1).
    local CurrentAltitude = self.Controllable:GetUnit(1):GetAltitude()
    local CurrentPointVec3 = POINT_VEC3:New( CurrentVec2.x, CurrentAltitude, CurrentVec2.y )
    local ToEngageZoneSpeed = self.PatrolMaxSpeed
    local CurrentRoutePoint = CurrentPointVec3:WaypointAir( 
        self.PatrolAltType, 
        POINT_VEC3.RoutePointType.TurningPoint, 
        POINT_VEC3.RoutePointAction.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 @{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 = POINT_VEC3:New( ToTargetVec2.x, self.EngageAltitude, ToTargetVec2.y )
    
    --- Create a route point of type air.
    local ToTargetRoutePoint = ToTargetPointVec3:WaypointAir( 
      self.PatrolAltType, 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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 Targetting
  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.
-- 
-- ===
-- 
-- ### [Demo Missions](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/master/FOR%20-%20Formation)
-- 
-- ===
-- 
-- ### [YouTube Playlist](https://www.youtube.com/playlist?list=PL7ZUrU4zZUl0bFIJ9jIdYM22uaWmIN4oz)
-- 
-- ===
-- 
-- ### 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 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.


--- Build large formations, make AI follow a @{Wrapper.Client#CLIENT} (player) leader or a @{Wrapper.Unit#UNIT} (AI) leader.
--
-- AI_FORMATION makes AI @{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 )
-- 
-- @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,
}

--- AI_FORMATION.Mode class
-- @type AI_FORMATION.MODE
-- @field #number FOLLOW
-- @field #number MISSION

--- MENUPARAM type
-- @type MENUPARAM
-- @field #AI_FORMATION ParamSelf
-- @field #Distance ParamDistance
-- @field #function ParamFunction
-- @field #string ParamMessage

--- 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.
-- @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:SetFlightRandomization( 2 )
  
  self:SetStartState( "None" ) 

  self:AddTransition( "*", "Stop", "Stopped" )

  self:AddTransition( "None", "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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group.
  -- @param #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
  -- @param #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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

--- 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 #nubmer YStart The start position on the Y-axis in meters for the first group.
-- @param #nubmer YSpace The space between groups on the Y-axis in meters for each sequent group.
-- @param #nubmer 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 ) --R2.1
  self:F( { FollowGroupSet, From , Event ,To, XStart, XSpace, YStart, YSpace, ZStart, 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 = POINT_VEC3: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
  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 #nubmer 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)

  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 #nubmer 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)

  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 #nubmer YStart The start position on the Y-axis in meters for the first group.
-- @param #nubmer 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)

  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 #nubmer YStart The start position on the Y-axis in meters for the first group.
-- @param #nubmer 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)

  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 #nubmer YStart The start position on the Y-axis in meters for the first group.
-- @param #nubmer 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)

  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 #nubmer YStart The start position on the Y-axis in meters for the first group.
-- @param #nubmer 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)

  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 #nubmer 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 #nubmer 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 = POINT_VEC3: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
  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 #nubmer 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 #nubmer 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 #nubmer 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 #nubmer 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 = POINT_VEC3: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
  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


--- @param Follow#AI_FORMATION self
function AI_FORMATION:onenterFollowing( FollowGroupSet ) --R2.1
  self:F( )

  self:T( { self.FollowUnit.UnitName, self.FollowUnit:IsAlive() } )
  if self.FollowUnit:IsAlive() then

    local ClientUnit = self.FollowUnit

    self:T( {ClientUnit.UnitName } )

    local CT1, CT2, CV1, CV2
    CT1 = ClientUnit:GetState( self, "CT1" )

    if CT1 == nil or CT1 == 0 then
      ClientUnit:SetState( self, "CV1", ClientUnit:GetPointVec3() )
      ClientUnit:SetState( self, "CT1", timer.getTime() )
    else
      CT1 = ClientUnit:GetState( self, "CT1" )
      CT2 = timer.getTime()
      CV1 = ClientUnit:GetState( self, "CV1" )
      CV2 = ClientUnit:GetPointVec3()
      
      ClientUnit:SetState( self, "CT1", CT2 )
      ClientUnit:SetState( self, "CV1", CV2 )
    end
        
    FollowGroupSet:ForEachGroup(
      --- @param Wrapper.Group#GROUP FollowGroup
      -- @param Wrapper.Unit#UNIT ClientUnit
      function( FollowGroup, Formation, ClientUnit, CT1, CV1, CT2, CV2 )
        
        FollowGroup:OptionROTEvadeFire()
        FollowGroup:OptionROEReturnFire()

        local GroupUnit = FollowGroup:GetUnit( 1 )
        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", GroupUnit:GetPointVec3() )
            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 = GroupUnit:GetPointVec3()
            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 ) )
            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 CVI = { x = CV2.x + CS * 10 * math.sin(Ca),
              y = GH2.y - ( Distance + FollowFormation.x ) / 5, -- + FollowFormation.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
            }
            
            if self.SmokeDirectionVector == true then
              trigger.action.smoke( GDV, trigger.smokeColor.Green )
              trigger.action.smoke( GDV_Formation, trigger.smokeColor.White )
            end
            
            
            
            local Time = 60
            
            local Speed = - ( Distance + FollowFormation.x ) / Time
            local GS = Speed + CS
            if Speed < 0 then
              Speed = 0
            end

            -- 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,
      self, ClientUnit, CT1, CV1, CT2, CV2
    )

    self:__Follow( -0.5 )
  end
  
end

--- **AI** -- (R2.4) - 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.
-- 
-- 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.
--    
-- @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
-- @param Core.Set#SET_CARGO CargoSet
-- @param #number CombatRadius
-- @return #AI_CARGO
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" )
  
  self:AddTransition( "Unloaded", "Pickup", "*" )
  self:AddTransition( "Loaded", "Deploy", "*" )
  
  self:AddTransition( "*", "Load", "Boarding" )
  self:AddTransition( { "Boarding", "Loaded" }, "Board", "Boarding" )
  self:AddTransition( "Boarding", "Loaded", "Boarding" )
  self:AddTransition( "Boarding", "PickedUp", "Loaded" )
  
  self:AddTransition( "Loaded", "Unload", "Unboarding" )
  self:AddTransition( "Unboarding", "Unboard", "Unboarding" )
  self:AddTransition( "Unboarding", "Unloaded", "Unboarding" )
  self:AddTransition( "Unboarding", "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
  
  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()
  
            -- Only when there is space within the bay to load the next cargo item!
            if Carrier_Weight[CarrierUnit] > CargoWeight then --and CargoBayFreeVolume > CargoVolume 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
            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:I({ 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.
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** -- (R2.4) - Models the intelligent transportation of infantry and other cargo.
--
-- ===
-- 
-- ### 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.
-- 
-- 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.
-- 
-- 
-- 
-- @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( "*", "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


--- 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


--- @param #AI_CARGO_APC 
-- @param Wrapper.Group#GROUP APC
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


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
      
      local Waypoints = APC:TaskGroundOnRoad( Coordinate, _speed, "Line abreast", true )
  
      local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Pickup", self, Coordinate, Speed, PickupZone )
      
      self:F({Waypoints = Waypoints})
      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
    
    local _speed=Speed or APC:GetSpeedMax()*0.5
     
    local Waypoints = APC:TaskGroundOnRoad( Coordinate, _speed, "Line abreast", true )

    local TaskFunction = APC:TaskFunction( "AI_CARGO_APC._Deploy", self, Coordinate, DeployZone )
    
    self:F({Waypoints = Waypoints})
    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.

    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** -- (R2.4) - Models the intelligent transportation of infantry (cargo).
--
-- ===
-- 
-- ### 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.
--  
-- 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 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 ... 
-- 
-- 
-- ===
-- 
-- @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" ) 
  
  self:AddTransition( "Unloaded", "Pickup", "*" )
  self:AddTransition( "Loaded", "Deploy", "*" )
  
  self:AddTransition( { "Unloaded", "Loading" }, "Load", "Boarding" )
  self:AddTransition( "Boarding", "Board", "Boarding" )
  self:AddTransition( "Boarding", "Loaded", "Boarding" )
  self:AddTransition( "Boarding", "PickedUp", "Loaded" )
  self:AddTransition( "Loaded", "Unload", "Unboarding" )
  self:AddTransition( "Unboarding", "Unboard", "Unboarding" )
  self:AddTransition( "Unboarding", "Unloaded", "Unboarding" )
  self:AddTransition( "Unboarding", "Deployed", "Unloaded" )

  self:AddTransition( "*", "Landed", "*" )
  self:AddTransition( "*", "Queue", "*" )
  self:AddTransition( "*", "Orbit" , "*" ) 
  self:AddTransition( "*", "Home" , "*" ) 
  
  self:AddTransition( "*", "Destroyed", "Destroyed" )

  --- 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 drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
  --- 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 drive 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 drive 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 drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  
  --- 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 drive 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 drive to the pickup coordinate. Default is 50% of max possible speed the unit can go.
  

  -- 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 )
  
  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


--- @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 )

  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< 5.
    -- Only then the infantry can unload (and load too, for consistency)!

    self:F( { Helicopter:GetName(), Height = Helicopter:GetHeight( true ), Velocity = Helicopter:GetVelocityKMH() } )

    if self.RoutePickup == true then
      if Helicopter:GetHeight( true ) <= 5 and Helicopter:GetVelocityKMH() < 10 then
        --self:Load( Helicopter:GetPointVec2() )
        self:Load( self.PickupZone )
        self.RoutePickup = false
      end
    end
    
    if self.RouteDeploy == true then
      if Helicopter:GetHeight( true ) <= 5 and Helicopter:GetVelocityKMH() < 10 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 )

  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", 
--            POINT_VEC3.RoutePointType.TurningPoint, 
--            POINT_VEC3.RoutePointAction.TurningPoint, 
--            Speed, 
--            true 
--          )
--          Route[#Route+1] = WaypointFrom
        local CoordinateTo   = Coordinate
        local WaypointTo = CoordinateTo:WaypointAir( 
          "RADIO", 
          POINT_VEC3.RoutePointType.TurningPoint, 
          POINT_VEC3.RoutePointAction.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 )

  if Helicopter and Helicopter:IsAlive() then
    
    local Route = {}
    
--          local CoordinateFrom = Helicopter:GetCoordinate()
--          local WaypointFrom = CoordinateFrom:WaypointAir( 
--            "RADIO", 
--            POINT_VEC3.RoutePointType.TurningPoint, 
--            POINT_VEC3.RoutePointAction.TurningPoint, 
--            Speed, 
--            true 
--          )
--          Route[#Route+1] = WaypointFrom
    local CoordinateTo   = Coordinate
    local WaypointTo = CoordinateTo:WaypointAir( 
      "RADIO", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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( { Helicopter, 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 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_HELICOPTER:onafterPickup( Helicopter, From, Event, To, 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()
    local CoordinateTo   = Coordinate

    --- Create a route point of type air.
    local WaypointFrom = CoordinateFrom:WaypointAir( 
      "RADIO", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.TurningPoint, 
      _speed, 
      true 
    )

    --- Create a route point of type air.
    local WaypointTo = CoordinateTo:WaypointAir( 
      "RADIO", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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 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 deploy coordinate.
function AI_CARGO_HELICOPTER:onafterDeploy( Helicopter, 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", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.TurningPoint, 
      _speed, 
      true 
    )
    Route[#Route+1] = WaypointFrom
    Route[#Route+1] = WaypointFrom

    --- Create a route point of type air.
    local CoordinateTo   = Coordinate
    local WaypointTo = CoordinateTo:WaypointAir( 
      "RADIO", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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 = {}
    
    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 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 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 )

  if Helicopter and Helicopter:IsAlive() ~= nil then

    self.RouteHome = true
     
    local Route = {}
    
    --- Calculate the target route point.

    Coordinate.y = Height
    
    Speed = Speed or Helicopter:GetSpeedMax()*0.5          

    --- Create a route point of type air.
    local CoordinateFrom = Helicopter:GetCoordinate()
    local WaypointFrom = CoordinateFrom:WaypointAir( 
      "RADIO", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.TurningPoint, 
      Speed , 
      true 
    )
    Route[#Route+1] = WaypointFrom

    --- Create a route point of type air.
    local CoordinateTo   = Coordinate
    local WaypointTo = CoordinateTo:WaypointAir( 
      "RADIO", 
      POINT_VEC3.RoutePointType.TurningPoint, 
      POINT_VEC3.RoutePointAction.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** -- (R2.4) - Models the intelligent transportation of infantry (cargo).
--
-- ===
-- 
-- ### 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.
--  
-- 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 ... 
-- 
-- 
-- @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 plane.
  -- @param #string From
  -- @param #string Event
  -- @param #string To
  -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up.
  -- @param #number Speed in km/h for travelling to pickup base.
  
  --- Pickup Trigger for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] Pickup
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up.
  -- @param #number Speed in km/h for travelling to pickup base.
  
  --- 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 Wrapper.Airbase#AIRBASE Airbase Airbase where troops are picked up.
  -- @param #number Speed in km/h for travelling to pickup base.
  
  --- 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
  -- @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.
  
  --- Deploy Trigger for AI_CARGO_AIRPLANE
  -- @function [parent=#AI_CARGO_AIRPLANE] Deploy
  -- @param #AI_CARGO_AIRPLANE self
  -- @param Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed.
  -- @param #number Speed Speed in km/h for travelling to deploy base.
  
  --- 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 Wrapper.Airbase#AIRBASE Airbase Destination airbase where troops are deployed.
  -- @param #number Speed Speed in km/h for travelling to deploy base.

  --- 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
  
  -- 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
-- @param #number 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 (optional) PickupZone The 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
    
    self.PickupZone = PickupZone
  
    -- 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 = 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())
    --env.info("Distance closest to pickup airbase = "..Dist)
    
    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, 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
-- @param #number Speed in km/h for travelling to pickup base.
-- @param #number Height Height in meters to move to the home coordinate.
-- @param Core.Zone#ZONE_AIRBASE DeployZone The 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 = DeployZone:GetAirbase()
    
    -- 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.
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(
      POINT_VEC3.RoutePointAltType.BARO,
      "Land",
      "Landing", 
      Speed or Airplane:GetSpeedMax()*0.8
    )    
    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** -- (R2.4) - 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/develop/AID%20-%20AI%20Dispatching/AID-CGO%20-%20AI%20Cargo%20Dispatching)
-- 
-- ===
-- 
-- # 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.
-- 
-- ===
-- 
-- ### 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 = {}
}

--- @field #list 
AI_CARGO_DISPATCHER.AI_Cargo = {}

--- @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:I( { 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
                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** -- (2.4) - 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.
-- 
-- 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.   
-- 
-- ===
-- 
-- @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

function AI_CARGO_DISPATCHER_APC:AICargo( APC, CargoSet )

  return AI_CARGO_APC:New( APC, CargoSet, self.CombatRadius )
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, 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( true )
-- 
-- -- Keep the cargo in the carrier when the carrier is under attack.
-- AICargoDispatcher:SetCombatRadius( false )
function AI_CARGO_DISPATCHER_APC:SetCombatRadius( CombatRadius )

  self.CombatRadius = CombatRadius or 0

  return self
end

--- **AI** -- (2.4) - 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.
-- 
-- 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.   
-- 
-- ===
-- 
-- @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( 0, 0 )
  self:SetDeployRadius( 0, 0 )
  
  self:SetPickupHeight( 500, 200 )
  self:SetDeployHeight( 500, 200 )
  
  return self
end


function AI_CARGO_DISPATCHER_HELICOPTER:AICargo( Helicopter, CargoSet )

  return AI_CARGO_HELICOPTER:New( Helicopter, CargoSet )
end

--- **AI** -- (R2.4) - 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.
-- 
-- 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!**
-- 
-- 
-- 
-- @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
--- (SP) (MP) (FSM) Accept or reject process for player (task) assignments.
-- 
-- ===
-- 
-- # @{#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.
-- 
-- ===
-- 
-- # 1) @{#ACT_ASSIGN_ACCEPT} class, extends @{Core.Fsm.Assign#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.Assign#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.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 )
  
    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.
-- 
-- ===
-- 
-- # @{#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 @{Core.Fsm.Route#ACT_ROUTE}
-- 
-- The ACT_ROUTE_ZONE class implements the core functions to route an AIR @{Wrapper.Controllable} player @{Wrapper.Unit} to a @{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.
-- 
-- ===
-- 
-- @module Actions.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( self.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 occuring on @{Wrapper.Unit}s.
-- 
-- ![Banner Image](..\Presentations\ACT_ACCOUNT\Dia1.JPG)
-- 
-- === 
-- 
-- @module Actions.Account
-- @image MOOSE.JPG

do -- ACT_ACCOUNT
  
  --- # @{#ACT_ACCOUNT} FSM class, extends @{Core.Fsm#FSM_PROCESS}
  -- 
  -- ## 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  
  -- 
  --   * **Asigned**: 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.
  --     
  -- @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, Event )
  
    self:__NoMore( 1 )
  end
  
end -- ACT_ACCOUNT

do -- ACT_ACCOUNT_DEADS

  --- # @{#ACT_ACCOUNT_DEADS} FSM class, extends @{Core.Fsm.Account#ACT_ACCOUNT}
  -- 
  -- The ACT_ACCOUNT_DEADS class accounts (detects, counts and reports) successful kills of DCS units.
  -- The process is given a @{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.
-- 
-- ## 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 @{Core.Fsm.Route#ACT_ASSIST}
-- 
-- The ACT_ASSIST_SMOKE_TARGETS_ZONE class implements the core functions to smoke targets in a @{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.
-- 
-- ===
-- 
-- @module Actions.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--- **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.
--  
-- 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.
-- 
-- @field #COMMANDCENTER
COMMANDCENTER = {
  ClassName = "COMMANDCENTER",
  CommandCenterName = "",
  CommandCenterCoalition = nil,
  CommandCenterPositionable = nil,
  Name = "",
  ReferencePoints = {},
  ReferenceNames = {},
  CommunicationMode = "80",
}

--- 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.AutoAssignTasks = false
	
	self.Missions = {}

  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 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.ReportMissionsStatus, 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 DCScoalition#coalition
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.AssignRandomTask, 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:AssignRandomTask( TaskGroup )

  local Tasks = {}

  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
      Tasks[#Tasks+1] = MissionTask
    end
  end
  
  local Task = Tasks[ math.random( 1, #Tasks ) ] -- Tasking.Task#TASK
  
  Task:SetAssignMethod( ACT_ASSIGN_MENU_ACCEPT:New( Task.TaskBriefing ) )
  
  Task:AssignToGroup( TaskGroup )

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.
-- @param #COMMANDCENTER self
-- @param #boolean AutoAssign true for ON and false or nil for OFF.
function COMMANDCENTER:SetAutoAssignTasks( AutoAssign )

  self.AutoAssignTasks = AutoAssign or false
  
  local GroupSet = self:AddGroups()

  for GroupID, TaskGroup in pairs( GroupSet:GetSet() ) do
    local TaskGroup = TaskGroup -- Wrapper.Group#GROUP
    self:GetMenu( TaskGroup )
  end

  if self.AutoAssignTasks == true then
    self:ScheduleRepeat( 10, 30, 0, nil, self.AssignTasks, self )
  else
    self:ScheduleStop( self.AssignTasks )
  end
  
  self:SetCommandCenterMenu()

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 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:AssignRandomTask( TaskGroup )
      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, 20, 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, 15, 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, 15, 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

--- **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.
-- 
-- 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 asigned 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.
-- 
-- 
-- @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 @{CLIENT}.
-- @param DCS#coaliton.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:I( { 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 @{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 @{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:I( string.format( "Mission %s is assigned to %s", MissionName, MissionGroupName ) )
    
    return self
  end
  
  --- Clear the @{Wrapper.Group} assignment from the @{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 @{Task} within the @{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 @{Task} ID to be completed within the @{Mission}. 
-- @param #MISSION self
-- @param Tasking.Task#TASK Task is the @{Task} object.
-- @return Tasking.Task#TASK The task added.
function MISSION:GetNextTaskID( Task )

  self.TaskNumber = self.TaskNumber + 1

  return self.TaskNumber
end


--- Register a @{Task} to be completed within the @{Mission}. 
-- Note that there can be multiple @{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 @{Task} object.
-- @return Tasking.Task#TASK The task added.
function MISSION:AddTask( Task )

  local TaskName = Task:GetTaskName()
  self:I( { "==> Adding TASK ", MissionName = self:GetName(), TaskName = TaskName } )

  self.Tasks[TaskName] = Task
  
  self:GetCommandCenter():SetMenu()

  return Task
end


--- Removes a @{Task} to be completed within the @{Mission}. 
-- Note that there can be multiple @{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 @{Task} object.
-- @return #nil The cleaned Task reference.
function MISSION:RemoveTask( Task )

  local TaskName = Task:GetTaskName()
  self:I( { "<== 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 @{Mission} **COMPLETED**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsCOMPLETED()
  return self:Is( "COMPLETED" )
end

--- Is the @{Mission} **IDLE**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsIDLE()
  return self:Is( "IDLE" )
end

--- Is the @{Mission} **ENGAGED**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsENGAGED()
  return self:Is( "ENGAGED" )
end

--- Is the @{Mission} **FAILED**.
-- @param #MISSION self
-- @return #boolean
function MISSION:IsFAILED()
  return self:Is( "FAILED" )
end

--- Is the @{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 TaskGoalTotal = Task:GetGoalTotal() or 0
    local TaskName = Task:GetName()
    PlayerList[TaskName] = PlayerList[TaskName] or {}
    if TaskGoalTotal ~= 0 then
      local PlayerNames = self:GetPlayerNames()
      for PlayerName, PlayerData in pairs( PlayerNames ) do
        PlayerList[TaskName][PlayerName] = string.format( 'Player (%s): Task "%s": %d%%', PlayerName, TaskName, Task:GetPlayerProgress( PlayerName ) * 100 / TaskGoalTotal )
      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 @{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.
-- 
-- ===
-- 
-- # 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 @{Tasking.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 %-tage 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 addtional **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 governer.
-- 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 awared, 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.
-- 
-- ===
-- 
-- ### Author: **FlightControl**
-- 
-- ### Contributions: 
-- 
-- ===
-- 
-- @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 Succcess 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()
    
    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()
  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 briefng 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" )
    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
function TASK:MenuTaskStatus( TaskGroup )

  local ReportText = self:ReportDetails( TaskGroup )
  
  self:T( ReportText )
  self:GetMission():GetCommandCenter():MessageTypeToGroup( ReportText, TaskGroup, MESSAGE.Type.Detailed )

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 @{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: Obscolete?
--- 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 Task@{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 Task@{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 Task@{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 Task@{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 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 Function.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
      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 ) 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}
-- 
-- ## The TASKINFO class implements the methods to contain information and display information of a task. 
-- 
-- @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 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 )
  self.VolatileInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail } )
  if Keep == true then
    self.PersistentInfo:Add( Key, { Data = Data, Order = Order, Detail = Detail } )
  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 )
  self:AddInfo( "Coordinate", Coordinate, Order, Detail, Keep )
  return self
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", ThreatText .. " [" .. string.rep(  "■", ThreatLevel ) .. string.rep(  "□", 10 - ThreatLevel ) .. "]", 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 Core.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

    self:F( { Key = Key, Detail = Detail, Data = Data } )
    
    if Data.Detail:find( Detail ) then
      local Text = ""
      if Key == "TaskName" then
        Key = nil
        Text = Data.Data
      end
      if Key == "Coordinate" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToString( ReportGroup:GetUnit(1), nil, Task )
      end
      if Key == "Threat" then
        local DataText = Data.Data -- #string
        Text = DataText
      end
      if Key == "Counting" then
        local DataText = Data.Data -- #string
        Text = DataText
      end
      if Key == "Targets" then
        local DataText = Data.Data -- #string
        Text = DataText
      end
      if Key == "QFE" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToStringPressure( ReportGroup:GetUnit(1), nil, Task )
      end
      if Key == "Temperature" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToStringTemperature( ReportGroup:GetUnit(1), nil, Task )
      end
      if Key == "Wind" then
        local Coordinate = Data.Data -- Core.Point#COORDINATE
        Text = Coordinate:ToStringWind( ReportGroup:GetUnit(1), nil, Task )
      end
      if Key == "Cargo" then
        local DataText = Data.Data -- #string
        Text = DataText
      end
      if Key == "Friendlies" then
        local DataText = Data.Data -- #string
        Text = DataText
      end
      if Key == "Players" then
        local DataText = Data.Data -- #string
        Text = DataText
      end


      if Line < math.floor( Data.Order / 10 ) then
        if Line == 0 then
          if Text ~= "" then
            Report:AddIndent( LineReport:Text( ", " ), "-" )
          end
        else
          if Text ~= "" then
            Report:AddIndent( LineReport:Text( ", " ) )
          end
        end
        LineReport = REPORT:New()
        Line = math.floor( Data.Order / 10 )
      end

      if Text ~= "" then
        LineReport:Add( ( Key and ( Key .. ":" ) or "" ) .. Text )
      end
    end
  end
  Report:AddIndent( LineReport:Text( ", " ) )

end
--- This module contains the TASK_MANAGER class and derived classes.
-- 
-- ===
-- 
-- 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.
-- 
-- ===
-- 
-- ### 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()
    self:E()
  
  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.
-- 
-- 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.
--    
--    
-- ===
-- 
-- ### 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.
  -- @extends Core.Fsm#FSM

  --- DETECTION_MANAGER class.
  -- @field #DETECTION_MANAGER
  DETECTION_MANAGER = {
    ClassName = "DETECTION_MANAGER",
    SetGroup = nil,
    Detection = 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
  
  --- 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 )
  	self:E()
  
  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 @{Detection}.
  -- @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) Supression 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 @{Detection} object.
  -- 
  -- 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  @{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 @{Client}s of the @{Wrapper.Group} @{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 @{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 @{CommandCenter}, which is orchestrating a @{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 Tacical 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 %-tage 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 @{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 @{CommandCenter} object. The master communication channel.
  --   - A @{Mission} object. Each task belongs to a Mission.
  --   - A @{Detection} object. There are several detection grouping methods to choose from.
  --   - A @{Task_A2G_Dispatcher} object. The master A2G task dispatcher.
  --   - A @{Set} of @{Wrapper.Group} objects that will detect the emeny, the RecceSet. This is attached to the @{Detection} object.
  --   - A @{Set} ob @{Wrapper.Group} objects that will attack the enemy, the AttackSet. This is attached to the @{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 recieve the dispatched Tasks. The DetectionAreas will report any detected enemies to the TaskDispatcher.
  -- 
  --   
  --
  -- @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.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
  
  
  --- 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
              Mission:GetCommandCenter():MessageToGroup( string.format( "Obsolete A2G task %s for %s removed.", TaskText, Mission:GetShortText() ), TaskGroup )
            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.ID
        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: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, TaskIndex )
                    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, TaskIndex )
                      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 ~= "" 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 )
            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 )
              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 )
                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 ~= "" 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 @{Set} of Target Units, 
  -- based on the tasking capabilities defined in @{Tasking.Task#TASK}.
  -- 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.
  -- 
  -- @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:Count()
  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 ) 
    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:Count() == 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: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
      self.TaskInfo:AddQFEAtCoordinate( TargetCoordinate, 30, "MOD" )
      self.TaskInfo:AddTemperatureAtCoordinate( TargetCoordinate, 31, "MD" )
      self.TaskInfo:AddWindAtCoordinate( TargetCoordinate, 32, "MD" )
    end
    
  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 @{Set} of EWR installation groups.
  -- 
  -- ![Banner Image](..\Presentations\TASK_A2A_DISPATCHER\Dia3.JPG)
  -- 
  -- The EWR will detect units, will group them, and will dispatch @{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 choosen, which is 6km.
  -- 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 @{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!
  -- 
  -- @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
    
    
    -- 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
  
  
  
  --- 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


    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

    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, Core.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, Core.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 ~= "" 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 @{Set} of Target Units, 
  -- based on the tasking capabilities defined in @{Tasking.Task#TASK}.
  -- 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.
  -- 
  -- @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 ) 
    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

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 urgen 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}
-- 
-- 
-- ===
-- 
-- ## Test Missions:
-- 
-- Test missions can be located on the main GITHUB site.
-- 
-- [FlightControl-Master/MOOSE_MISSIONS/TAD - Task Dispatching/CGO - Cargo Dispatching/](https://github.com/FlightControl-Master/MOOSE_MISSIONS/tree/develop/TAD%20-%20Task%20Dispatching/CGO%20-%20Cargo%20Dispatching)
-- 
-- ===
--
-- ## 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 @{Cargo.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.  
-- ![Task_Types](../Tasking/Task_Cargo_Settings.JPG)  
-- 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.
--   
-- 
-- ===
-- 
-- ### 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 Core.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 Core.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 Core.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 Core.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 Core.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
  
  
  
end 


--- **Tasking** -- Models tasks for players to transport cargo.
-- 
-- **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.
--
-- ===
-- 
-- 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 @{Core.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 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 "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 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!
  -- 
  -- 
  -- ## 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 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 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 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_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.
--
-- ===
-- 
-- 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
-- 
-- ===
-- 
-- ### 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 @{Core.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 Core.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 Core.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 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_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 -- Core.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. 
--  
-- 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.
--   
-- ===
-- 
-- ### 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 @{Core.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 @{Core.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#Country 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.SetCargo#SET_CARGO SetCargo The SetCargo to be transported.
  -- @param #string Briefing The briefing of the task transport to be shown to the player.
  -- @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 )

    self.TransportCount = self.TransportCount + 1
    
    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()
    
    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
  -- @return #boolean Return true if you want the task assigning to continue... false will cancel the loop.
  function TASK_CARGO_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.
--          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 ~= "" 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.TaskZoneCapture
-- @image MOOSE.JPG

do -- TASK_ZONE_GOAL

  --- The TASK_ZONE_GOAL class
  -- @type TASK_ZONE_GOAL
  -- @field Core.ZoneGoal#ZONE_GOAL ZoneGoal
  -- @extends Tasking.Task#TASK

  --- # TASK_ZONE_GOAL class, extends @{Tasking.Task#TASK}
  -- 
  -- 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.
  -- 
  -- @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 Core.ZoneGoal#ZONE_GOAL 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:AddProcess   ( "Planned", "Accept", ACT_ASSIGN_ACCEPT:New( self.TaskBriefing ), { Assigned = "StartMonitoring", Rejected = "Reject" }  )
    
    Fsm:AddTransition( "Assigned", "StartMonitoring", "Monitoring" )
    Fsm:AddTransition( "Monitoring", "Monitor", "Monitoring", {} )
    Fsm:AddTransition( "Monitoring", "RouteTo", "Monitoring" )
    Fsm:AddProcess( "Monitoring", "RouteToZone", ACT_ROUTE_ZONE:New(), {} )
    
    --Fsm:AddTransition( "Accounted", "DestroyedAll", "Accounted" )
    --Fsm:AddTransition( "Accounted", "Success", "Success" )
    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_ZONE_GOAL Task
    function Fsm:onafterStartMonitoring( TaskUnit, Task )
      self:F( { self } )
      self:__Monitor( 0.1 )
      self:__RouteTo( 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:__RouteTo( 0.1 )
      end
    end

    return self
 
  end

  --- @param #TASK_ZONE_GOAL self
  -- @param Core.ZoneGoal#ZONE_GOAL ZoneGoal The ZoneGoal Engine.
  function TASK_ZONE_GOAL:SetProtect( ZoneGoal )
  
    self.ZoneGoal = ZoneGoal -- Core.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_ZONE_CAPTURE

  --- The TASK_ZONE_CAPTURE class
  -- @type TASK_ZONE_CAPTURE
  -- @field Core.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoal
  -- @extends #TASK_ZONE_GOAL

  --- # TASK_ZONE_CAPTURE class, extends @{Tasking.TaskZoneGoal#TASK_ZONE_GOAL}
  -- 
  -- The TASK_ZONE_CAPTURE 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_ZONE_CAPTURE is used by the @{Tasking.Task_A2G_Dispatcher#TASK_A2G_DISPATCHER} to automatically create SEAD tasks 
  -- based on detected enemy ground targets.
  -- 
  -- @field #TASK_ZONE_CAPTURE
  TASK_ZONE_CAPTURE = {
    ClassName = "TASK_ZONE_CAPTURE",
  }
  

  --- Instantiates a new TASK_ZONE_CAPTURE.
  -- @param #TASK_ZONE_CAPTURE 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.ZoneGoalCoalition#ZONE_GOAL_COALITION ZoneGoalCoalition
  -- @param #string TaskBriefing The briefing of the task.
  -- @return #TASK_ZONE_CAPTURE self
  function TASK_ZONE_CAPTURE:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, TaskBriefing)
    local self = BASE:Inherit( self, TASK_ZONE_GOAL:New( Mission, SetGroup, TaskName, ZoneGoalCoalition, "CAPTURE", TaskBriefing ) ) -- #TASK_ZONE_CAPTURE
    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()

    return self
  end 


  --- Instantiates a new TASK_ZONE_CAPTURE.
  -- @param #TASK_ZONE_CAPTURE self
  function TASK_ZONE_CAPTURE:UpdateTaskInfo() 


    local ZoneCoordinate = self.ZoneGoal:GetZone():GetCoordinate() 
    self.TaskInfo:AddCoordinate( ZoneCoordinate, 0, "SOD" )
    self.TaskInfo:AddText( "Zone Name", self.ZoneGoal:GetZoneName(), 10, "MOD" )
    self.TaskInfo:AddText( "Zone Coalition", self.ZoneGoal:GetCoalitionName(), 11, "MOD" )
  end
    

  function TASK_ZONE_CAPTURE:ReportOrder( ReportGroup ) 
    local Coordinate = self:GetData( "Coordinate" )
    --local Coordinate = self.TaskInfo.Coordinates.TaskInfoText
    local Distance = ReportGroup:GetCoordinate():Get2DDistance( Coordinate )
    
    return Distance
  end
  
  
  --- @param #TASK_ZONE_CAPTURE self
  -- @param Wrapper.Unit#UNIT TaskUnit
  function TASK_ZONE_CAPTURE:OnAfterGoal( From, Event, To, PlayerUnit, PlayerName )
  
    self:F( { PlayerUnit = PlayerUnit } )
    
    if self.ZoneGoal then
      if self.ZoneGoal.Goal:IsAchieved() then
        self:Success()
        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
      end
    end
    
    self:__Goal( -10, PlayerUnit, PlayerName )
  end

end

-- 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.Timer#SCHEDULEDISPATCHER

--- Declare the main database object, which is used internally by the MOOSE classes.
_DATABASE = DATABASE:New() -- Core.Database#DATABASE

_SETTINGS = SETTINGS:Set()
_SETTINGS:SetPlayerMenuOn()

_DATABASE:_RegisterCargos()
_DATABASE:_RegisterZones()

BASE:TraceOnOff( false )
env.info( '*** MOOSE INCLUDE END *** ' )
