Files
MOOSE/Moose Development/Moose/Ops/Intelligence.lua
T
Applevangelist 8ede104a3d xx
2026-03-19 12:27:50 +01:00

3153 lines
110 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
--- **Ops** - Office of Military Intelligence.
--
-- **Main Features:**
--
-- * Detect and track contacts consistently
-- * Detect and track clusters of contacts consistently
-- * Use FSM events to link functionality into your scripts
-- * Easy setup
--
-- ===
--
-- ### Author: **funkyfranky**
-- @module Ops.Intel
-- @image OPS_Intel.png
--- INTEL class.
-- @type INTEL
-- @field #string ClassName Name of the class.
-- @field #number verbose Verbosity level.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number coalition Coalition side number, e.g. `coalition.side.RED`.
-- @field #string alias Name of the agency.
-- @field Core.Set#SET_GROUP detectionset Set of detection groups, aka agents.
-- @field #table filterCategory Filter for unit categories.
-- @field #table filterCategoryGroup Filter for group categories.
-- @field Core.Set#SET_ZONE acceptzoneset Set of accept zones. If defined, only contacts in these zones are considered.
-- @field Core.Set#SET_ZONE rejectzoneset Set of reject zones. Contacts in these zones are not considered, even if they are in accept zones.
-- @field Core.Set#SET_ZONE conflictzoneset Set of conflict zones. Contacts in these zones are considered, even if they are not in accept zones or if they are in reject zones.
-- @field Core.Set#SET_ZONE corridorzoneset Set of corridor zones. Contacts in these zones are never considered. Also see corridorfloorheight and corridorfloorceiling.
-- @field #number corridorfloor [Air] Contacts below this height (ASL!) are considered, even if they are in a corridor zone.
-- @field #number corridorceiling [Air] Contacts above this height (ASL!) are considered, even if they are in a corridor zone.
-- @field #table Contacts Table of detected items.
-- @field #table ContactsLost Table of lost detected items.
-- @field #table ContactsUnknown Table of new detected items.
-- @field #table Clusters Clusters of detected groups.
-- @field #boolean clusteranalysis If true, create clusters of detected targets.
-- @field #boolean clustermarkers If true, create cluster markers on F10 map.
-- @field #number clustercounter Running number of clusters.
-- @field #number dTforget Time interval in seconds before a known contact which is not detected any more is forgotten.
-- @field #number clusterradius Radius in meters in which groups/units are considered to belong to a cluster.
-- @field #number prediction Seconds default to be used with CalcClusterFuturePosition.
-- @field #boolean detectStatics If `true`, detect STATIC objects. Default `false`.
-- @field #number statusupdate Time interval in seconds after which the status is refreshed. Default 60 sec. Should be negative.
-- @field #boolean DetectAccoustic If true, also detect by sound (ie proximity).
-- @field #number DetectAccousticRadius Radius dfor accoustic detection, defaults to 2000 meters.
-- @field #table DetectAccousticUnitTypes Types of units we can detect accousticly. Defaults to {Unit.Category.HELICOPTER}
-- @extends Core.Fsm#FSM
--- Top Secret!
--
-- ===
--
-- # The INTEL Concept
--
-- * Lightweight replacement for @{Functional.Detection#DETECTION}
-- * Detect and track contacts consistently
-- * Detect and track clusters of contacts consistently
-- * Once detected and still alive, planes will be tracked 10 minutes, helicopters 20 minutes, ships and trains 1 hour, ground units 2 hours
-- * Use FSM events to link functionality into your scripts
--
-- # Basic Usage
--
-- ## Set up a detection SET_GROUP
--
-- Red_DetectionSetGroup = SET_GROUP:New()
-- Red_DetectionSetGroup:FilterPrefixes( { "Red EWR" } )
-- Red_DetectionSetGroup:FilterOnce()
--
-- ## New Intel type detection for the red side, logname "KGB"
--
-- RedIntel = INTEL:New(Red_DetectionSetGroup, "red", "KGB")
-- RedIntel:SetClusterAnalysis(true, true)
-- RedIntel:SetVerbosity(2)
-- RedIntel:__Start(2)
--
-- ## Hook into new contacts found
--
-- function RedIntel:OnAfterNewContact(From, Event, To, Contact)
-- local text = string.format("NEW contact %s detected by %s", Contact.groupname, Contact.recce or "unknown")
-- MESSAGE:New(text, 15, "KGB"):ToAll()
-- end
--
-- ## And/or new clusters found
--
-- function RedIntel:OnAfterNewCluster(From, Event, To, Cluster)
-- local text = string.format("NEW cluster #%d of size %d", Cluster.index, Cluster.size)
-- MESSAGE:New(text,15,"KGB"):ToAll()
-- end
--
--
-- @field #INTEL
INTEL = {
ClassName = "INTEL",
verbose = 0,
lid = nil,
alias = nil,
filterCategory = {},
detectionset = nil,
Contacts = {},
ContactsLost = {},
ContactsUnknown = {},
Clusters = {},
clustercounter = 1,
clusterradius = 15000,
clusteranalysis = true,
clustermarkers = false,
clusterarrows = false,
prediction = 300,
detectStatics = false,
DetectAccoustic = false,
DetectAccousticRadius = 1000,
DetectAccousticUnitTypes = {Unit.Category.HELICOPTER},
DopplerRadar = true,
DopplerMinAltAGL = 500,
DopplerNotchSin = math.sin(math.rad(15)),
DopplerMinSpeedMps = 50,
DopplerRCS = true,
DopplerRadarRangeM = 200 * 1000,
}
--- Detected item info.
-- @type INTEL.Contact
-- @field #string groupname Name of the group.
-- @field Wrapper.Group#GROUP group The contact group.
-- @field #string typename Type name of detected item.
-- @field #number category Category number.
-- @field #string categoryname Category name.
-- @field #string attribute Generalized attribute.
-- @field #number threatlevel Threat level of this item.
-- @field #number Tdetected Time stamp in abs. mission time seconds when this item was last detected.
-- @field Core.Point#COORDINATE position Last known position of the item.
-- @field DCS#Vec3 velocity 3D velocity vector. Components x,y and z in m/s.
-- @field #number speed Last known speed in m/s.
-- @field #boolean isship If `true`, contact is a naval group.
-- @field #boolean ishelo If `true`, contact is a helo group.
-- @field #boolean isground If `true`, contact is a ground group.
-- @field #boolean isStatic If `true`, contact is a STATIC object.
-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this contact.
-- @field Ops.Target#TARGET target The Target attached to this contact.
-- @field #string recce The name of the recce unit that detected this contact.
-- @field #string ctype Contact type of #INTEL.Ctype.
-- @field #string platform [AIR] Contact platform name, e.g. Foxbat, Flanker_E, defaults to Bogey if unknown
-- @field #number heading [AIR] Heading of the contact, if available.
-- @field #boolean maneuvering [AIR] Contact has changed direction by >10 deg.
-- @field #number altitude [AIR] Flight altitude of the contact in meters.
--- Cluster info.
-- @type INTEL.Cluster
-- @field #number index Cluster index.
-- @field #number size Number of groups in the cluster.
-- @field #table Contacts Table of contacts in the cluster.
-- @field #number threatlevelMax Max threat level of cluster.
-- @field #number threatlevelSum Sum of threat levels.
-- @field #number threatlevelAve Average of threat levels.
-- @field Core.Point#COORDINATE coordinate Coordinate of the cluster.
-- @field Wrapper.Marker#MARKER marker F10 marker.
-- @field #number markerID Marker ID.
-- @field Ops.Auftrag#AUFTRAG mission The current Auftrag attached to this cluster.
-- @field #string ctype Cluster type of #INTEL.Ctype.
-- @field #number altitude [AIR] Average flight altitude of the cluster in meters.
--- Contact or cluster type.
-- @type INTEL.Ctype
-- @field #string GROUND Ground.
-- @field #string NAVAL Ship.
-- @field #string AIRCRAFT Airpane or helicopter.
-- @field #string STRUCTURE Static structure.
INTEL.Ctype={
GROUND="Ground",
NAVAL="Naval",
AIRCRAFT="Aircraft",
STRUCTURE="Structure"
}
--- INTEL class version.
-- @field #string version
INTEL.version="0.3.10"
---
-- ══════════════════════════════════════════════════════════════════
-- INTEL Doppler radar extension
--
-- Models four phenomena of a 1970/80s pulse-Doppler ground radar
-- (representative types: Soviet P-18 Spoon Rest, P-37 Bar Lock,
-- P-80 Back Net / NATO AN/TPS-43 / Hughes AN/TPS-70):
--
-- A) GROUND CLUTTER (AGL threshold)
-- Low-flying targets blend into terrain returns. Below DopplerMinAltAGL
-- detection probability drops linearly to 0 at 0 m AGL.
--
-- B) VELOCITY NOTCH (beam aspect)
-- The MTI (Moving Target Indicator) filter suppresses returns with
-- near-zero Doppler shift. Targets flying perpendicular to the radar
-- beam (radial-velocity fraction < sin(NotchHalfDeg)) are rejected.
-- Classic P-18/P-37 notch was ≈ ±1218° around 90° aspect.
--
-- C) MINIMUM SPEED GATE
-- Very slow targets (taxiing aircraft, hovering) cannot be separated
-- from ground clutter by their Doppler shift alone.
--
-- D) RADAR CROSS SECTION (RCS)
-- Larger targets are detectable at longer ranges. The radar range
-- equation gives R_max ∝ σ^0.25, so detection range is scaled by
-- (σ / σ_ref)^0.25 relative to a reference aircraft (default: 5 m²).
-- RCS also varies with aspect: nose-on ≈ 15% of side-on value.
-- Known DCS aircraft values are stored in INTEL.RCS_Table; unknowns
-- fall back to a category default (fighter/bomber/helicopter).
-- Values are approximate averages from public IISS/Jane's data.
-- ══════════════════════════════════════════════════════════════════
--
-- ── RCS lookup table (nominal side-on RCS in m²) ─────────────────
-- Frontal (nose-on / tail-on) RCS is modelled as 15% of these values
-- via aspect interpolation in _GetAspectRCS().
-- Sources: public declassified estimates, Jane's, IISS assessments.
--- @field INTEL.RCS_Table
INTEL.RCS_Table = {
-- ── US / NATO fixed-wing ──────────────────────────────────────
["A-10C"] = 8.0, -- large, flat surfaces, no LO shaping
["A-10C_2"] = 8.0,
["F-14A-135-GR"] = 6.0, -- variable-sweep; larger than F-16
["F-14B"] = 6.0,
["F-15C"] = 5.0,
["F-15E"] = 5.0, -- CFTs add modest signature
["F-15ESE"] = 5.0,
["F-16A"] = 1.2,
["F-16C bl.50"] = 1.2,
["F-16C bl.52d"] = 1.2,
["F/A-18C"] = 1.5,
["FA-18C_hornet"] = 1.5,
["F/A-18C_hornet"] = 1.5,
["F/A-18F"] = 2.0, -- slightly larger two-seater
["F-117A"] = 0.003, -- faceted LO design
["F-22A"] = 0.0001,-- VLO
["F-35A"] = 0.001, -- VLO, approx
["B-52H"] = 100.0, -- very large, many flat reflectors
["B-1B"] = 0.75, -- blended-wing LO shaping
["B-2A"] = 0.001, -- VLO flying wing
["AV8BNA"] = 2.0,
["Harrier"] = 2.0,
["A-4E-C"] = 3.0,
["Tornado_IDS"] = 5.0,
["Tornado_GR4"] = 5.0,
["F-111F"] = 5.0,
["F-4E"] = 6.0, -- large, blunt nose
["F-5E"] = 1.0, -- small fighter
["F-5E-3"] = 1.0,
["Mirage-F1CE"] = 2.5,
["Mirage-F1EE"] = 2.5,
["M-2000C"] = 2.0,
["M-2000-5"] = 2.0,
["C-17A"] = 50.0,
["C-130"] = 40.0,
["KC-130"] = 40.0,
["KC-135"] = 50.0,
["IL-76MD"] = 45.0,
["E-3A"] = 50.0, -- plus large rotodome
-- ── Soviet / Russian fixed-wing ──────────────────────────────
["MiG-15bis"] = 4.0,
["MiG-19P"] = 3.5,
["MiG-21Bis"] = 2.5, -- small delta
["MiG-23MLD"] = 7.0, -- variable-sweep, large intakes
["MiG-25PD"] = 14.0, -- very large, all-metal, Mach-3 design
["MiG-25RBT"] = 14.0,
["MiG-29A"] = 5.0,
["MiG-29S"] = 5.0,
["MiG-29G"] = 5.0,
["MiG-29K"] = 4.0,
["MiG-31"] = 14.0, -- similar to MiG-25
["Su-7B"] = 6.0,
["Su-17M4"] = 7.0, -- variable-sweep
["Su-24M"] = 6.0,
["Su-24MR"] = 6.0,
["Su-25"] = 10.0,
["Su-25T"] = 10.0,
["Su-25TM"] = 10.0,
["Su-27"] = 15.0,
["Su-30"] = 15.0,
["Su-33"] = 15.0, -- wing fold + canards
["Su-34"] = 10.0, -- some reduction vs Su-27
["Su-57"] = 0.01, -- PAK-FA LO shaping
["Tu-22M3"] = 20.0,
["Tu-95MS"] = 80.0,
["Tu-142"] = 80.0,
["Tu-160"] = 12.0, -- blended wing reduces vs Tu-95
["An-26B"] = 30.0,
["An-30M"] = 30.0,
["IL-78M"] = 45.0,
["A-50"] = 50.0, -- plus rotodome
-- ── Helicopters ──────────────────────────────────────────────
["Mi-8MT"] = 5.0,
["Mi-8MSB"] = 5.0,
["Mi-8MSB-V"] = 5.0,
["Mi-8AMTSh"] = 5.0,
["Mi-24V"] = 3.5,
["Mi-24P"] = 3.5,
["Mi-28N"] = 2.5,
["Ka-50"] = 2.0,
["Ka-52"] = 2.0,
["AH-64D"] = 3.5,
["AH-64D_BLK_II"] = 3.5,
["UH-1H"] = 3.0,
["UH-60L"] = 3.0,
["CH-47D"] = 8.0, -- large tandem-rotor
["OH-58D"] = 0.8, -- small scout
["SA342M"] = 0.8,
["SA342L"] = 0.8,
}
---
-- Category-based defaults for aircraft types not in the table.
-- Keyed by DCS Group.Category integer.
--- @type INTEL.RCS_CategoryDefault
-- @field #number Group.Category.AIRPLANE RCS Airplane (fightrt) fallback == 5
-- @field #number Group.Category.HELICOPTER RCS Helo fallback == 2.5
INTEL.RCS_CategoryDefault = {
[Group.Category.AIRPLANE] = 5.0, -- generic fighter-sized
[Group.Category.HELICOPTER] = 2.5, -- generic helicopter
}
---
-- Reference RCS (m²) for range scaling. Detection range in SetDopplerRadar
-- is the range at which this reference aircraft is reliably detected.
-- @field INTEL.RCS_Reference
INTEL.RCS_Reference = 5.0 -- m²
---
-- Nose-on/tail-on RCS as a fraction of the side-on value.
-- Public estimates for conventional (non-LO) aircraft: ~0.100.20.
-- @field INTEL.RCS_NoseOnFraction
INTEL.RCS_NoseOnFraction = 0.15
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- ToDo list
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- TODO: Add min cluster size. Only create new clusters if they have a certain group size.
-- NODO: process detected set asynchroniously for better performance.
-- DONE: Add statics.
-- DONE: Filter detection methods.
-- DONE: Accept zones.
-- DONE: Reject zones.
-- NOGO: SetAttributeZone --> return groups of generalized attributes in a zone.
-- DONE: Loose units only if they remain undetected for a given time interval. We want to avoid fast oscillation between detected/lost states. Maybe 1-5 min would be a good time interval?!
-- DONE: Combine units to groups for all, new and lost.
-- DONE: Add corridor zones.
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Constructor
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Create a new INTEL object and start the FSM.
-- @param #INTEL self
-- @param Core.Set#SET_GROUP DetectionSet Set of detection groups.
-- @param #number Coalition Coalition side. Can also be passed as a string "red", "blue" or "neutral".
-- @param #string Alias An *optional* alias how this object is called in the logs etc.
-- @return #INTEL self
function INTEL:New(DetectionSet, Coalition, Alias)
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, FSM:New()) -- #INTEL
-- Detection set.
self.detectionset=DetectionSet or SET_GROUP:New()
if Coalition and type(Coalition)=="string" then
if Coalition=="blue" then
Coalition=coalition.side.BLUE
elseif Coalition=="red" then
Coalition=coalition.side.RED
elseif Coalition=="neutral" then
Coalition=coalition.side.NEUTRAL
else
self:E("ERROR: Unknown coalition in INTEL!")
end
end
-- Determine coalition from first group in set.
self.coalition=Coalition or DetectionSet:CountAlive()>0 and DetectionSet:GetFirst():GetCoalition() or nil
-- Filter coalition.
if self.coalition then
local coalitionname=UTILS.GetCoalitionName(self.coalition):lower()
self.detectionset:FilterCoalitions(coalitionname)
end
-- Filter once.
self.detectionset:FilterOnce()
-- Set alias.
if Alias then
self.alias=tostring(Alias)
else
self.alias="INTEL SPECTRE"
if self.coalition then
if self.coalition==coalition.side.RED then
self.alias="INTEL KGB"
elseif self.coalition==coalition.side.BLUE then
self.alias="INTEL CIA"
end
end
end
self.DetectVisual = true
self.DetectOptical = true
self.DetectRadar = true
self.DetectIRST = true
self.DetectRWR = true
self.DetectDLINK = true
self.statusupdate = -60
-- Set some string id for output to DCS.log file.
self.lid=string.format("%s (%s) | ", self.alias, self.coalition and UTILS.GetCoalitionName(self.coalition) or "unknown")
-- Start State.
self:SetStartState("Stopped")
-- Add FSM transitions.
-- From State --> Event --> To State
self:AddTransition("Stopped", "Start", "Running") -- Start FSM.
self:AddTransition("*", "Status", "*") -- INTEL status update.
self:AddTransition("*", "Stop", "Stopped") -- Stop FSM.
self:AddTransition("*", "Detect", "*") -- Start detection run. Not implemented yet!
self:AddTransition("*", "NewContact", "*") -- New contact has been detected.
self:AddTransition("*", "LostContact", "*") -- Contact could not be detected any more.
self:AddTransition("*", "NewCluster", "*") -- New cluster has been detected.
self:AddTransition("*", "LostCluster", "*") -- Cluster could not be detected any more.
-- Defaults
self:SetForgetTime()
self:SetAcceptZones()
self:SetRejectZones()
self:SetCorridorZones()
self:SetConflictZones()
------------------------
--- Pseudo Functions ---
------------------------
--- Triggers the FSM event "Start". Starts the INTEL. Initializes parameters and starts event handlers.
-- @function [parent=#INTEL] Start
-- @param #INTEL self
--- Triggers the FSM event "Start" after a delay. Starts the INTEL. Initializes parameters and starts event handlers.
-- @function [parent=#INTEL] __Start
-- @param #INTEL self
-- @param #number delay Delay in seconds.
--- Triggers the FSM event "Stop". Stops the INTEL and all its event handlers.
-- @param #INTEL self
--- Triggers the FSM event "Stop" after a delay. Stops the INTEL and all its event handlers.
-- @function [parent=#INTEL] __Stop
-- @param #INTEL self
-- @param #number delay Delay in seconds.
--- Triggers the FSM event "Status".
-- @function [parent=#INTEL] Status
-- @param #INTEL self
--- Triggers the FSM event "Status" after a delay.
-- @function [parent=#INTEL] __Status
-- @param #INTEL self
-- @param #number delay Delay in seconds.
--- Triggers the FSM event "NewContact".
-- @function [parent=#INTEL] NewContact
-- @param #INTEL self
-- @param #INTEL.Contact Contact Detected contact.
--- Triggers the FSM event "NewContact" after a delay.
-- @function [parent=#INTEL] NewContact
-- @param #INTEL self
-- @param #number delay Delay in seconds.
-- @param #INTEL.Contact Contact Detected contact.
--- On After "NewContact" event.
-- @function [parent=#INTEL] OnAfterNewContact
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Contact Contact Detected contact.
--- Triggers the FSM event "LostContact".
-- @function [parent=#INTEL] LostContact
-- @param #INTEL self
-- @param #INTEL.Contact Contact Lost contact.
--- Triggers the FSM event "LostContact" after a delay.
-- @function [parent=#INTEL] LostContact
-- @param #INTEL self
-- @param #number delay Delay in seconds.
-- @param #INTEL.Contact Contact Lost contact.
--- On After "LostContact" event.
-- @function [parent=#INTEL] OnAfterLostContact
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Contact Contact Lost contact.
--- Triggers the FSM event "NewCluster".
-- @function [parent=#INTEL] NewCluster
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster Detected cluster.
--- Triggers the FSM event "NewCluster" after a delay.
-- @function [parent=#INTEL] NewCluster
-- @param #INTEL self
-- @param #number delay Delay in seconds.
-- @param #INTEL.Cluster Cluster Detected cluster.
--- On After "NewCluster" event.
-- @function [parent=#INTEL] OnAfterNewCluster
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Cluster Cluster Detected cluster.
--- Triggers the FSM event "LostCluster".
-- @function [parent=#INTEL] LostCluster
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster Lost cluster.
-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.
--- Triggers the FSM event "LostCluster" after a delay.
-- @function [parent=#INTEL] LostCluster
-- @param #INTEL self
-- @param #number delay Delay in seconds.
-- @param #INTEL.Cluster Cluster Lost cluster.
-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.
--- On After "LostCluster" event.
-- @function [parent=#INTEL] OnAfterLostCluster
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Cluster Cluster Lost cluster.
-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- User functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Set accept zones. Only contacts detected in this/these zone(s) are considered.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE AcceptZoneSet Set of accept zones.
-- @return #INTEL self
function INTEL:SetAcceptZones(AcceptZoneSet)
self.acceptzoneset=AcceptZoneSet or SET_ZONE:New()
return self
end
--- Set to accept accoustic detection.
-- @param #INTEL self
-- @param #number Radius (Optional) Radius in which we can "hear" units. Defaults to 1000 meters.
-- @param #table UnitCategories(Optional) Set what Unit Categories we can "hear". Defaults to `{Unit.Category.GROUND_UNIT,Unit.Category.HELICOPTER}`
-- @return #INTEL self
function INTEL:SetAccousticDetectionOn(Radius,UnitCategories)
self.DetectAccoustic = true
self.DetectAccousticRadius = Radius or 1000
self.DetectAccousticUnitTypes = UnitCategories or {Unit.Category.HELICOPTER}
return self
end
--- Switch off accoustic detection.
-- @param #INTEL self
-- @return #INTEL self
function INTEL:SetAccousticDetectionOff()
self.DetectAccoustic = false
return self
end
--- Add an accept zone. Only contacts detected in this zone are considered.
-- @param #INTEL self
-- @param Core.Zone#ZONE AcceptZone Add a zone to the accept zone set.
-- @return #INTEL self
function INTEL:AddAcceptZone(AcceptZone)
self.acceptzoneset:AddZone(AcceptZone)
return self
end
--- Remove an accept zone from the accept zone set.
-- @param #INTEL self
-- @param Core.Zone#ZONE AcceptZone Remove a zone from the accept zone set.
-- @return #INTEL self
function INTEL:RemoveAcceptZone(AcceptZone)
self.acceptzoneset:Remove(AcceptZone:GetName(), true)
return self
end
--- Set reject zones. Contacts detected in this/these zone(s) are rejected and not reported by the detection.
-- Note that reject zones overrule accept zones, i.e. if a unit is inside an accept zone and inside a reject zone, it is rejected.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE RejectZoneSet Set of reject zone(s).
-- @return #INTEL self
function INTEL:SetRejectZones(RejectZoneSet)
self.rejectzoneset=RejectZoneSet or SET_ZONE:New()
return self
end
--- Add a reject zone. Contacts detected in this zone are rejected and not reported by the detection.
-- Note that reject zones overrule accept zones, i.e. if a unit is inside an accept zone and inside a reject zone, it is rejected.
-- @param #INTEL self
-- @param Core.Zone#ZONE RejectZone Add a zone to the reject zone set.
-- @return #INTEL self
function INTEL:AddRejectZone(RejectZone)
self.rejectzoneset:AddZone(RejectZone)
return self
end
--- Remove a reject zone from the reject zone set.
-- @param #INTEL self
-- @param Core.Zone#ZONE RejectZone Remove a zone from the reject zone set.
-- @return #INTEL self
function INTEL:RemoveRejectZone(RejectZone)
self.rejectzoneset:Remove(RejectZone:GetName(), true)
return self
end
--- Set conflict zones. Contacts detected in this/these zone(s) are reported by the detection.
-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE ConflictZoneSet Set of conflict zone(s).
-- @return #INTEL self
function INTEL:SetConflictZones(ConflictZoneSet)
self.conflictzoneset=ConflictZoneSet or SET_ZONE:New()
return self
end
--- Add a conflict zone. Contacts detected in this zone are conflicted and not reported by the detection.
-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone.
-- @param #INTEL self
-- @param Core.Zone#ZONE ConflictZone Add a zone to the conflict zone set.
-- @return #INTEL self
function INTEL:AddConflictZone(ConflictZone)
self.conflictzoneset:AddZone(ConflictZone)
return self
end
--- Remove a conflict zone from the conflict zone set.
-- Note that conflict zones overrule all other zones, i.e. if a unit is outside of an accept zone and inside a reject zone, it is still reported if inside a conflict zone.
-- @param #INTEL self
-- @param Core.Zone#ZONE ConflictZone Remove a zone from the conflict zone set.
-- @return #INTEL self
function INTEL:RemoveConflictZone(ConflictZone)
self.conflictzoneset:Remove(ConflictZone:GetName(), true)
return self
end
--- Set corrdidor zones. Contacts detected in this/these zone(s) are never reported by the detection.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Set#SET_ZONE CorridorZoneSet Set of corrdidor zone(s).
-- @return #INTEL self
function INTEL:SetCorridorZones(CorridorZoneSet)
self.corridorzoneset=CorridorZoneSet or SET_ZONE:New()
return self
end
--- Add a corrdidor zone. Contacts detected in this zone are corrdidored and not reported by the detection.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Zone#ZONE CorridorZone Add a zone to the corrdidor zone set.
-- @return #INTEL self
function INTEL:AddCorridorZone(CorridorZone)
self.corridorzoneset:AddZone(CorridorZone)
return self
end
--- Remove a corrdidor zone from the corrdidor zone set.
-- Note that corrdidor zones overrule all other zones, for exceptions see corridor floor and corridor ceiling heights.
-- @param #INTEL self
-- @param Core.Zone#ZONE CorridorZone Remove a zone from the corrdidor zone set.
-- @return #INTEL self
function INTEL:RemoveCorridorZone(CorridorZone)
self.corridorzoneset:Remove(CorridorZone:GetName(), true)
return self
end
--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values.
-- Overrides corridor exception for objects flying outside this limitations.
-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object:
-- `mycorridorzone:SetProperty("CorridorFloor",500)` -- meters, case sensitivity matters!
-- `mycorridorzone:SetProperty("CorridorCeiling",10000)` -- meters, case sensitivity matters!
-- @param #INTEL self
-- @param #number Floor Floor altitude in meters.
-- @param #number Ceiling Ceiling altitude in meters.
-- @return #INTEL self
function INTEL:SetCorridorLimits(Floor,Ceiling)
self.corridorceiling = Ceiling or 10000
self.corridorfloor = Floor or 1
return self
end
--- [Air] Add corrdidor zone floor and height. This is generally applicable to all(!) corridor zones. Considered as ASL (above sea level or barometric) values.
-- Overrides corridor exception for objects flying outside this limitations.
-- To set an individual ceiling/floor on any Core.Zone#ZONE you wish to use, set these properties on the Core.Zone#ZONE object:
-- `mycorridorzone:SetProperty("CorridorFloor",UTILS.FeetToMeters(5000))` -- feet, case sensitivity matters!
-- `mycorridorzone:SetProperty("CorridorCeiling",UTILS.FeetToMeters(20000))` -- feet, case sensitivity matters!
-- @param #INTEL self
-- @param #number Floor Floor altitude in feet.
-- @param #number Ceiling Ceiling altitude in feet.
-- @return #INTEL self
function INTEL:SetCorridorLimitsFeet(Floor,Ceiling)
local Ceiling = Ceiling or 25000
local Floor = Floor or 15000
self.corridorceiling = UTILS.FeetToMeters(Ceiling)
self.corridorfloor = UTILS.FeetToMeters(Floor)
return self
end
--- **OBSOLETE, not functional!** Set forget contacts time interval.
-- Previously known contacts that are not detected any more, are "lost" after this time.
-- This avoids fast oscillations between a contact being detected and undetected.
-- @param #INTEL self
-- @param #number TimeInterval (Optional) Time interval in seconds. Default is 120 sec.
-- @return #INTEL self
function INTEL:SetForgetTime(TimeInterval)
return self
end
--- Filter unit categories. Valid categories are:
--
-- * Unit.Category.AIRPLANE
-- * Unit.Category.HELICOPTER
-- * Unit.Category.GROUND_UNIT
-- * Unit.Category.SHIP
-- * Unit.Category.STRUCTURE
--
-- @param #INTEL self
-- @param #table Categories Filter categories, e.g. {Unit.Category.AIRPLANE, Unit.Category.HELICOPTER}.
-- @return #INTEL self
function INTEL:SetFilterCategory(Categories)
if type(Categories)~="table" then
Categories={Categories}
end
self.filterCategory=Categories
local text="Filter categories: "
for _,category in pairs(self.filterCategory) do
text=text..string.format("%d,", category)
end
self:T(self.lid..text)
return self
end
--- Method to make the radar detection less accurate, e.g. for WWII scenarios.
-- @param #INTEL self
-- @param #number minheight (Optional) Minimum flight height to be detected, in meters AGL (above ground). Defaults to 250m.
-- @param #number thresheight (Optional) Threshold to escape the radar if flying below minheight, defaults to 90 (90% escape chance)
-- @param #number thresblur (Optional) Threshold to be detected by the radar overall, defaults to 85 (85% chance to be found)
-- @param #number closing (Optional) Closing-in in km - the limit of km from which on it becomes increasingly difficult to escape radar detection if flying towards the radar position. Should be about 1/3 of the radar detection radius in kilometers, defaults to 20.
-- @return #INTEL self
function INTEL:SetRadarBlur(minheight,thresheight,thresblur,closing)
self.RadarBlur = true
self.RadarBlurMinHeight = minheight or 250 -- meters
self.RadarBlurThresHeight = thresheight or 90 -- 10% chance to find a low flying group
self.RadarBlurThresBlur = thresblur or 85 -- 25% chance to escape the radar overall
self.RadarBlurClosing = closing or 20 -- 20km
self.RadarBlurClosingSquare = self.RadarBlurClosing * self.RadarBlurClosing
return self
end
--- Set the accept range in kilometers from each of the recce. Only object closer than this range will be detected.
-- @param #INTEL self
-- @param #number Range Range in kilometers
-- @return #INTEL self
function INTEL:SetAcceptRange(Range)
self.RadarAcceptRange = true
self.RadarAcceptRangeKilometers = Range or 75
return self
end
--- Filter group categories. Valid categories are:
--
-- * Group.Category.AIRPLANE
-- * Group.Category.HELICOPTER
-- * Group.Category.GROUND
-- * Group.Category.SHIP
-- * Group.Category.TRAIN
--
-- @param #INTEL self
-- @param #table GroupCategories Filter categories, e.g. `{Group.Category.AIRPLANE, Group.Category.HELICOPTER}`.
-- @return #INTEL self
function INTEL:FilterCategoryGroup(GroupCategories)
if type(GroupCategories)~="table" then
GroupCategories={GroupCategories}
end
self.filterCategoryGroup=GroupCategories
local text="Filter group categories: "
for _,category in pairs(self.filterCategoryGroup) do
text=text..string.format("%d,", category)
end
self:T(self.lid..text)
return self
end
--- Add a group to the detection set.
-- @param #INTEL self
-- @param Wrapper.Group#GROUP AgentGroup Group of agents. Can also be an @{Ops.OpsGroup#OPSGROUP} object.
-- @return #INTEL self
function INTEL:AddAgent(AgentGroup)
-- Check if this was an OPS group.
if AgentGroup:IsInstanceOf("OPSGROUP") then
AgentGroup=AgentGroup:GetGroup()
end
-- Add to detection set.
self.detectionset:AddGroup(AgentGroup,true)
return self
end
--- Enable or disable cluster analysis of detected targets.
-- Targets will be grouped in coupled clusters.
-- @param #INTEL self
-- @param #boolean Switch If true, enable cluster analysis.
-- @param #boolean Markers If true, place markers on F10 map.
-- @param #boolean Arrows If true, draws arrows on F10 map.
-- @return #INTEL self
function INTEL:SetClusterAnalysis(Switch, Markers, Arrows)
self.clusteranalysis=Switch
self.clustermarkers=Markers
self.clusterarrows=Arrows
return self
end
--- Set whether STATIC objects are detected.
-- @param #INTEL self
-- @param #boolean Switch If `true`, statics are detected.
-- @return #INTEL self
function INTEL:SetDetectStatics(Switch)
if Switch and Switch==true then
self.detectStatics=true
else
self.detectStatics=false
end
return self
end
--- Set verbosity level for debugging.
-- @param #INTEL self
-- @param #number Verbosity The higher, the noisier, e.g. 0=off, 2=debug
-- @return #INTEL self
function INTEL:SetVerbosity(Verbosity)
self.verbose=Verbosity or 2
return self
end
--- Add a Mission (Auftrag) to a contact for tracking.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact
-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this contact
-- @return #INTEL self
function INTEL:AddMissionToContact(Contact, Mission)
if Mission and Contact then
Contact.mission = Mission
end
return self
end
--- Add a Mission (Auftrag) to a cluster for tracking.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster
-- @param Ops.Auftrag#AUFTRAG Mission The mission connected with this cluster
-- @return #INTEL self
function INTEL:AddMissionToCluster(Cluster, Mission)
if Mission and Cluster then
Cluster.mission = Mission
end
return self
end
--- Change radius of the Clusters.
-- @param #INTEL self
-- @param #number radius (Optional) The radius of the clusters in kilometers. Default 15 km.
-- @return #INTEL self
function INTEL:SetClusterRadius(radius)
self.clusterradius = (radius or 15)*1000
return self
end
--- Set detection types for this #INTEL - all default to true.
-- @param #INTEL self
-- @param #boolean DetectVisual Visual detection
-- @param #boolean DetectOptical Optical detection
-- @param #boolean DetectRadar Radar detection
-- @param #boolean DetectIRST IRST detection
-- @param #boolean DetectRWR RWR detection
-- @param #boolean DetectDLINK Data link detection
-- @return self
function INTEL:SetDetectionTypes(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK)
self.DetectVisual = DetectVisual and true
self.DetectOptical = DetectOptical and true
self.DetectRadar = DetectRadar and true
self.DetectIRST = DetectIRST and true
self.DetectRWR = DetectRWR and true
self.DetectDLINK = DetectDLINK and true
return self
end
--- Get table of #INTEL.Contact objects
-- @param #INTEL self
-- @return #table Contacts or nil if not running
function INTEL:GetContactTable()
if self:Is("Running") then
return self.Contacts
else
return nil
end
end
--- Get table of #INTEL.Cluster objects
-- @param #INTEL self
-- @return #table Clusters or nil if not running
function INTEL:GetClusterTable()
if self:Is("Running") and self.clusteranalysis then
return self.Clusters
else
return nil
end
end
--- Get name of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #string Name of the contact.
function INTEL:GetContactName(Contact)
return Contact.groupname
end
--- Get group of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return Wrapper.Group#GROUP Group object.
function INTEL:GetContactGroup(Contact)
return Contact.group
end
--- Get threatlevel of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #number Threat level.
function INTEL:GetContactThreatlevel(Contact)
return Contact.threatlevel
end
--- Get type name of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #string Type name.
function INTEL:GetContactTypeName(Contact)
return Contact.typename
end
--- Get category name of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #string Category name.
function INTEL:GetContactCategoryName(Contact)
return Contact.categoryname
end
--- Get coordinate of a contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return Core.Point#COORDINATE Coordinates.
function INTEL:GetContactCoordinate(Contact)
return Contact.position
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Start & Status
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- On after Start event. Starts the FLIGHTGROUP FSM and event handlers.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function INTEL:onafterStart(From, Event, To)
-- Short info.
local text=string.format("Starting INTEL v%s", self.version)
self:I(self.lid..text)
-- Start the status monitoring.
self:__Status(-math.random(10))
return self
end
--- On after "Status" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
function INTEL:onafterStatus(From, Event, To)
-- FSM state.
local fsmstate=self:GetState()
-- Fresh arrays.
self.ContactsLost={}
self.ContactsUnknown={}
-- Check if group has detected any units.
self:UpdateIntel()
-- Number of total contacts.
local Ncontacts=#self.Contacts
local Nclusters=#self.Clusters
-- Short info.
if self.verbose>=1 then
local text=string.format("Status %s [Agents=%s]: Contacts=%d, Clusters=%d, New=%d, Lost=%d", fsmstate, self.detectionset:CountAlive(), Ncontacts, Nclusters, #self.ContactsUnknown, #self.ContactsLost)
self:I(self.lid..text)
end
-- Detailed info.
if self.verbose>=2 and Ncontacts>0 then
local text="Detected Contacts:"
for _,_contact in pairs(self.Contacts) do
local contact=_contact --#INTEL.Contact
local dT=timer.getAbsTime()-contact.Tdetected
text=text..string.format("\n- %s (%s): %s, units=%d, T=%d sec", contact.categoryname, contact.attribute, contact.groupname, contact.isStatic and 1 or contact.group:CountAliveUnits(), dT)
if contact.mission then
local mission=contact.mission --Ops.Auftrag#AUFTRAG
text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown")
end
end
self:I(self.lid..text)
end
self:__Status(self.statusupdate)
return self
end
--- Update detected items.
-- @param #INTEL self
function INTEL:UpdateIntel()
-- Set of all detected units.
local DetectedUnits={}
-- Set of which units was detected by which recce
local RecceDetecting = {}
-- Loop over all units providing intel.
for _,_group in pairs(self.detectionset.Set or {}) do
local group=_group --Wrapper.Group#GROUP
if group and group:IsAlive() then
for _,_recce in pairs(group:GetUnits()) do
local recce=_recce --Wrapper.Unit#UNIT
-- Get detected units.
if self.DopplerRadar then
self:GetDetectedUnitsDoppler(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK)
else
self:GetDetectedUnits(recce, DetectedUnits, RecceDetecting, self.DetectVisual, self.DetectOptical, self.DetectRadar, self.DetectIRST, self.DetectRWR, self.DetectDLINK)
end
end
if self.DetectAccoustic then
local recce = group:GetFirstUnitAlive()
local detectionzone = group:GetProperty("INTEL_DETECT_ACCZONE")
if not detectionzone then
detectionzone = ZONE_GROUP:New(group.IdentifiableName.."INTEL_DETECT_ACCZONE",group,self.DetectAccousticRadius or 2000)
group:SetProperty("INTEL_DETECT_ACCZONE",detectionzone)
end
if recce and recce:IsGround() then
self:GetDetectedUnitsAccoustic(recce,DetectedUnits,RecceDetecting,detectionzone)
end
end
end
end
local remove={}
for unitname,_unit in pairs(DetectedUnits) do
local unit=_unit --Wrapper.Unit#UNIT
local inconflictzone=false
-- Check if unit is in any of the conflict zones.
if self.conflictzoneset:Count()>0 then
for _,_zone in pairs(self.conflictzoneset.Set) do
local zone=_zone --Core.Zone#ZONE
if unit:IsInZone(zone) then
inconflictzone=true
break
end
end
end
-- Check if unit is in any of the accept zones.
if self.acceptzoneset:Count()>0 then
local inzone=false
for _,_zone in pairs(self.acceptzoneset.Set) do
local zone=_zone --Core.Zone#ZONE
if unit:IsInZone(zone) then
inzone=true
break
end
end
-- Unit is not in accept zone ==> remove!
if (not inzone) and (not inconflictzone) then
table.insert(remove, unitname)
end
end
-- Check if unit is in any of the reject zones.
if self.rejectzoneset:Count()>0 then
local inzone=false
for _,_zone in pairs(self.rejectzoneset.Set) do
local zone=_zone --Core.Zone#ZONE
if unit:IsInZone(zone) then
inzone=true
break
end
end
-- Unit is inside a reject zone ==> remove!
if inzone and (not inconflictzone) then
table.insert(remove, unitname)
end
end
-- Check if unit is in any of the corridor zones.
if self.corridorzoneset:Count()>0 then
self:T("Corridorzone Check for unit "..unit:GetName())
local inzone = false
for _,_zone in pairs(self.corridorzoneset.Set) do
local zone=_zone --Core.Zone#ZONE
if unit:IsInZone(zone) then
local corridorfloor = zone:GetProperty("CorridorFloor") or self.corridorfloor
local corridorceiling = zone:GetProperty("CorridorCeiling") or self.corridorceiling
local debugtext = "Corridorzone Check for unit "..unit:GetName().."\n"
debugtext = debugtext .. string.format("IsAir %s | Alt %dft | Floor %dft | Ceil %dft",tostring(unit:IsAir()),tonumber(UTILS.MetersToFeet(unit:GetAltitude())),
tonumber(UTILS.MetersToFeet(corridorfloor)),tonumber(UTILS.MetersToFeet(corridorceiling)))
MESSAGE:New(debugtext,15,"INTEL"):ToAllIf(self.verbose>1):ToLogIf(self.verbose>1)
if unit:IsAir() and (corridorfloor ~= nil or corridorceiling ~= nil) then
local alt = unit:GetAltitude()
if corridorfloor and alt > corridorfloor then inzone = true end
if corridorceiling and (inzone == true or corridorfloor == nil) and alt < corridorceiling then inzone = true else inzone = false end
if inzone == true then break end
else
inzone=true
break
end
end
end
-- Unit is inside a corridor zone ==> remove!
if inzone then
table.insert(remove, unitname)
end
end
-- Filter unit categories. Added check that we have a UNIT and not a STATIC object because :GetUnitCategory() is only available for units.
if #self.filterCategory>0 and unit:IsInstanceOf("UNIT") then
local unitcategory=unit:GetUnitCategory()
local keepit=false
for _,filtercategory in pairs(self.filterCategory) do
if unitcategory==filtercategory then
keepit=true
break
end
end
if not keepit then
self:T(self.lid..string.format("Removing unit %s category=%d", unitname, unit:GetCategory()))
table.insert(remove, unitname)
end
end
end
-- Remove filtered units.
for _,unitname in pairs(remove) do
DetectedUnits[unitname]=nil
end
-- Create detected groups.
local DetectedGroups={}
local DetectedStatics={}
local RecceGroups={}
for unitname,_unit in pairs(DetectedUnits) do
local unit=_unit --Wrapper.Unit#UNIT
if unit:IsInstanceOf("UNIT") then
local group=unit:GetGroup()
if group then
local groupname = group:GetName()
DetectedGroups[groupname]=group
RecceGroups[groupname]=RecceDetecting[unitname]
end
else
if self.detectStatics then
DetectedStatics[unitname]=unit
RecceGroups[unitname]=RecceDetecting[unitname]
end
end
end
-- Create detected contacts.
self:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceGroups)
-- Paint a picture of the battlefield.
if self.clusteranalysis then
self:PaintPicture()
end
return self
end
--- Update an #INTEL.Contact item.
-- @param #INTEL self
-- @param #INTEL.Contact Contact Contact.
-- @return #INTEL.Contact The contact.
function INTEL:_UpdateContact(Contact)
if Contact.isStatic then
-- Statics don't need to be updated.
else
if Contact.group and Contact.group:IsAlive() then
Contact.Tdetected=timer.getAbsTime()
Contact.position=Contact.group:GetCoordinate()
Contact.velocity=Contact.group:GetVelocityVec3()
Contact.speed=Contact.group:GetVelocityMPS()
if Contact.group:IsAir() then
Contact.altitude=Contact.group:GetAltitude()
local oldheading = Contact.heading or 1
local newheading = Contact.group:GetHeading()
if newheading == 0 then newheading = 1 end
local changeh = math.abs(((oldheading - newheading) + 360) % 360)
Contact.heading = newheading
if changeh > 10 then
Contact.maneuvering = true
else
Contact.maneuvering = false
end
end
end
end
return self
end
--- Create an #INTEL.Contact item from a given GROUP or STATIC object.
-- @param #INTEL self
-- @param Wrapper.Positionable#POSITIONABLE Positionable The GROUP or STATIC object.
-- @param #string RecceName The name of the recce group that has detected this contact.
-- @return #INTEL.Contact The contact.
function INTEL:_CreateContact(Positionable, RecceName)
if Positionable and Positionable:IsAlive() then
-- Create new contact.
local item={} --#INTEL.Contact
if Positionable:IsInstanceOf("GROUP") then
local group=Positionable --Wrapper.Group#GROUP
item.groupname=group:GetName()
item.group=group
item.Tdetected=timer.getAbsTime()
item.typename=group:GetTypeName()
item.attribute=group:GetAttribute()
item.category=group:GetCategory()
item.categoryname=group:GetCategoryName()
item.threatlevel=group:GetThreatLevel()
item.position=group:GetCoordinate()
item.velocity=group:GetVelocityVec3()
item.speed=group:GetVelocityMPS()
item.recce=RecceName
item.isground = group:IsGround() or false
item.isship = group:IsShip() or false
item.isStatic=false
if group:IsAir() then
item.platform=group:GetNatoReportingName()
item.heading = group:GetHeading()
item.maneuvering = false
item.altitude = group:GetAltitude()
else
-- TODO optionally add ground types?
item.platform="Unknown"
item.altitude = group:GetAltitude(true)
end
if item.category==Group.Category.AIRPLANE or item.category==Group.Category.HELICOPTER then
item.ctype=INTEL.Ctype.AIRCRAFT
elseif item.category==Group.Category.GROUND or item.category==Group.Category.TRAIN then
item.ctype=INTEL.Ctype.GROUND
elseif item.category==Group.Category.SHIP then
item.ctype=INTEL.Ctype.NAVAL
end
return item
elseif Positionable:IsInstanceOf("STATIC") then
local static=Positionable --Wrapper.Static#STATIC
item.groupname=static:GetName()
item.group=static
item.Tdetected=timer.getAbsTime()
item.typename=static:GetTypeName() or "Unknown"
item.attribute="Static"
item.category=3 --static:GetCategory()
item.categoryname=static:GetCategoryName() or "Unknown"
item.threatlevel=static:GetThreatLevel() or 0
item.position=static:GetCoord()
item.velocity=static:GetVelocityVec3()
item.speed=0
item.recce=RecceName
item.isground = true
item.isship = false
item.isStatic=true
item.ctype=INTEL.Ctype.STRUCTURE
return item
else
self:E(self.lid..string.format("ERROR: object needs to be a GROUP or STATIC!"))
end
end
return nil
end
--- Create detected items.
-- @param #INTEL self
-- @param #table DetectedGroups Table of detected Groups.
-- @param #table DetectedStatics Table of detected Statics.
-- @param #table RecceDetecting Table of detecting recce names.
function INTEL:CreateDetectedItems(DetectedGroups, DetectedStatics, RecceDetecting)
self:F({RecceDetecting=RecceDetecting})
-- Current time.
local Tnow=timer.getAbsTime()
-- Loop over groups.
for groupname,_group in pairs(DetectedGroups) do
local group=_group --Wrapper.Group#GROUP
-- Create or update contact for this group.
self:KnowObject(group, RecceDetecting[groupname])
end
-- Loop over statics.
for staticname,_static in pairs(DetectedStatics) do
local static=_static --Wrapper.Static#STATIC
-- Create or update contact for this group.
self:KnowObject(static, RecceDetecting[staticname])
end
-- Now check if there some groups could not be detected any more.
for i=#self.Contacts,1,-1 do
local item=self.Contacts[i] --#INTEL.Contact
-- Check if deltaT>Tforget. We dont want quick oscillations between detected and undetected states.
if self:_CheckContactLost(item) then
-- Trigger LostContact event. This also adds the contact to the self.ContactsLost table.
self:LostContact(item)
-- Remove contact from table.
self:RemoveContact(item)
end
end
return self
end
--- (Internal) Return the detected target groups of the controllable as a table.
-- The optional parameters specify the detection methods that can be applied.
-- If no detection method is given, the detection will use all the available methods by default.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT Unit The unit detecting.
-- @param #table DetectedUnits Table of detected units to be filled.
-- @param #table RecceDetecting Table of recce per unit to be filled.
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
function INTEL:GetDetectedUnits(Unit, DetectedUnits, RecceDetecting, DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK)
-- Get detected DCS units.
local reccename = Unit:GetName()
local detectedtargets=Unit:GetDetectedTargets(DetectVisual, DetectOptical, DetectRadar, DetectIRST, DetectRWR, DetectDLINK)
for DetectionObjectID, Detection in pairs(detectedtargets or {}) do
local DetectedObject=Detection.object -- DCS#Object
-- NOTE: Got an object that exists but when trying UNIT:Find() the DCS getName() function failed. ID of the object was 5,000,031
if DetectedObject and DetectedObject:isExist() and DetectedObject.id_<50000000 then
-- Protected call to get the name of the object.
local status,name = pcall(
function()
local name=DetectedObject:getName()
return name
end)
if status then
local unit=UNIT:FindByName(name)
if unit and unit:IsAlive() then
local DetectionAccepted = true
if self.RadarAcceptRange then
local reccecoord = Unit:GetCoord()
local coord = unit:GetCoord()
local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km
if dist > self.RadarAcceptRangeKilometers then DetectionAccepted = false end
end
if self.RadarBlur then
local reccecoord = Unit:GetCoord()
local coord = unit:GetCoord()
local dist = math.floor(coord:Get2DDistance(reccecoord)/1000) -- km
local AGL = unit:GetAltitude(true)
local minheight = self.RadarBlurMinHeight or 250 -- meters
local thresheight = self.RadarBlurThresHeight or 90 -- 10% chance to find a low flying group
local thresblur = self.RadarBlurThresBlur or 85 -- 25% chance to escape the radar overall
--local dist = math.floor(Distance)
if dist <= self.RadarBlurClosing then
thresheight = (((dist*dist)/self.RadarBlurClosingSquare)*thresheight)
thresblur = (((dist*dist)/self.RadarBlurClosingSquare)*thresblur)
end
local fheight = math.floor(math.random(1,10000)/100)
local fblur = math.floor(math.random(1,10000)/100)
if fblur > thresblur then DetectionAccepted = false end
if AGL <= minheight and fheight < thresheight then DetectionAccepted = false end
if self.debug or self.verbose > 1 then
MESSAGE:New("Radar Blur",10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
MESSAGE:New("Unit "..name.." is at "..math.floor(AGL).."m. Distance "..math.floor(dist).."km.",10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
MESSAGE:New(string.format("fheight = %d/%d | fblur = %d/%d",fheight,thresheight,fblur,thresblur),10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
MESSAGE:New("Detection Accepted = "..tostring(DetectionAccepted),10):ToLogIf(self.debug):ToAllIf(self.verbose>1)
end
end
if DetectionAccepted then
DetectedUnits[name]=unit
RecceDetecting[name]=reccename
self:T(string.format("Unit %s detect by %s", name, reccename))
end
else
if self.detectStatics then
local static=STATIC:FindByName(name, false)
if static then
--env.info("FF found static "..name)
DetectedUnits[name]=static
RecceDetecting[name]=reccename
end
end
end
else
-- Warning!
self:T(self.lid..string.format("WARNING: Could not get name of detected object ID=%s! Detected by %s", DetectedObject.id_, reccename))
end
end
end
end
--- (Internal) Return the detected target groups of the controllable as a @{Core.Set#SET_GROUP}.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT Recce The unit detecting.
-- @param #table DetectedUnits Table of detected units to be filled.
-- @param #table RecceDetecting Table of recce per unit to be filled.
-- @param Core.Zone#ZONE_GROUP detectionzone The zone where to look.
function INTEL:GetDetectedUnitsAccoustic(Recce,DetectedUnits,RecceDetecting,detectionzone)
local othercoalition = self.coalition == coalition.side.BLUE and coalition.side.RED or coalition.side.BLUE
self:T("Other coalition = "..othercoalition)
if detectionzone then
-- Get detected units
local reccename = Recce:GetName()
local DetectAccousticUnitTypes = self.DetectAccousticUnitTypes or {Unit.Category.HELICOPTER}
detectionzone:Scan({Object.Category.UNIT},DetectAccousticUnitTypes)
local unitset = detectionzone:GetScannedSetUnit(othercoalition) -- Core.Set#SET_UNIT
self:T("Accoustic detection found #Units "..unitset:CountAlive())
for _,_unit in pairs(unitset.Set or {}) do
if _unit and _unit:IsAlive() and _unit:GetCoalition() ~= self.coalition then
local name = _unit:GetName() or "none"
DetectedUnits[name]=_unit
RecceDetecting[name]=reccename
self:T("Unit name = "..name)
end
end
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- FSM Events
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- On after "NewContact" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Contact Contact Detected contact.
function INTEL:onafterNewContact(From, Event, To, Contact)
-- Debug text.
self:F(self.lid..string.format("NEW contact %s", Contact.groupname))
-- Add to table of unknown contacts.
table.insert(self.ContactsUnknown, Contact)
return self
end
--- On after "LostContact" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Contact Contact Lost contact.
function INTEL:onafterLostContact(From, Event, To, Contact)
-- Debug text.
self:F(self.lid..string.format("LOST contact %s", Contact.groupname))
-- Add to table of lost contacts.
table.insert(self.ContactsLost, Contact)
return self
end
--- On after "NewCluster" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Cluster Cluster Detected cluster.
function INTEL:onafterNewCluster(From, Event, To, Cluster)
-- Debug text.
self:F(self.lid..string.format("NEW cluster #%d [%s] of size %d", Cluster.index, Cluster.ctype, Cluster.size))
-- Add cluster to table.
self:_AddCluster(Cluster)
return self
end
--- On after "LostCluster" event.
-- @param #INTEL self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #INTEL.Cluster Cluster Lost cluster.
-- @param Ops.Auftrag#AUFTRAG Mission The Auftrag connected with this cluster or `nil`.
function INTEL:onafterLostCluster(From, Event, To, Cluster, Mission)
-- Debug text.
local text = self.lid..string.format("LOST cluster #%d [%s]", Cluster.index, Cluster.ctype)
if Mission then
local mission=Mission --Ops.Auftrag#AUFTRAG
text=text..string.format(" mission name=%s type=%s target=%s", mission.name, mission.type, mission:GetTargetName() or "unknown")
end
self:T(text)
return self
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Misc Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- Make the INTEL aware of a object that was not detected (yet). This will add the object to the contacts table and trigger a `NewContact` event.
-- @param #INTEL self
-- @param Wrapper.Positionable#POSITIONABLE Positionable Group or static object.
-- @param #string RecceName Name of the recce group that detected this object.
-- @param #number Tdetected (Optional) Abs. mission time in seconds, when the object is detected. Default now.
-- @return #INTEL self
function INTEL:KnowObject(Positionable, RecceName, Tdetected)
local Tnow=timer.getAbsTime()
Tdetected=Tdetected or Tnow
if Positionable and Positionable:IsAlive() then
if Tdetected>Tnow then
-- Delay call.
self:ScheduleOnce(Tdetected-Tnow, self.KnowObject, self, Positionable, RecceName)
else
-- Name of the object.
local name=Positionable:GetName()
-- Try to get the contact by name.
local contact=self:GetContactByName(name)
if contact then
-- Update contact info.
self:_UpdateContact(contact)
else
-- Create new contact.
contact=self:_CreateContact(Positionable, RecceName)
if contact then
-- Debug info.
self:T(string.format("%s contact detected by %s", contact.groupname, RecceName or "unknown"))
-- Add contact to table.
self:AddContact(contact)
-- Trigger new contact event.
self:NewContact(contact)
end
end
end
end
return self
end
--- Get a contact by name.
-- @param #INTEL self
-- @param #string groupname Name of the contact group.
-- @return #INTEL.Contact The contact.
function INTEL:GetContactByName(groupname)
for i,_contact in pairs(self.Contacts) do
local contact=_contact --#INTEL.Contact
if contact.groupname==groupname then
return contact
end
end
return nil
end
--- Check if a Contact is already known. It is checked, whether the contact is in the contacts table.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be added.
-- @return #boolean If `true`, contact is already known.
function INTEL:_IsContactKnown(Contact)
for i,_contact in pairs(self.Contacts) do
local contact=_contact --#INTEL.Contact
if contact.groupname==Contact.groupname then
return true
end
end
return false
end
--- Add a contact to our list.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be added.
-- @return #INTEL self
function INTEL:AddContact(Contact)
-- First check if the contact is already in the table.
if self:_IsContactKnown(Contact) then
self:E(self.lid..string.format("WARNING: Contact %s is already in the contact table!", tostring(Contact.groupname)))
else
self:T(self.lid..string.format("Adding new Contact %s to table", tostring(Contact.groupname)))
table.insert(self.Contacts, Contact)
end
return self
end
--- Remove a contact from our list.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be removed.
function INTEL:RemoveContact(Contact)
for i,_contact in pairs(self.Contacts) do
local contact=_contact --#INTEL.Contact
if contact.groupname==Contact.groupname then
table.remove(self.Contacts, i)
end
end
return self
end
--- Check if a contact was lost.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact to be removed.
-- @return #boolean If true, contact was not detected for at least *dTforget* seconds.
function INTEL:_CheckContactLost(Contact)
-- Group dead?
if Contact.group==nil or not Contact.group:IsAlive() then
return true
end
-- We never forget statics as they don't move.
if Contact.isStatic then
return false
end
-- Time since last detected.
local dT=timer.getAbsTime()-Contact.Tdetected
local dTforget=nil
if Contact.category==Group.Category.GROUND then
dTforget=60*60*2 -- 2 hours
elseif Contact.category==Group.Category.AIRPLANE then
dTforget=60*10 -- 10 min
elseif Contact.category==Group.Category.HELICOPTER then
dTforget=60*20 -- 20 min
elseif Contact.category==Group.Category.SHIP then
dTforget=60*60 -- 1 hour
elseif Contact.category==Group.Category.TRAIN then
dTforget=60*60 -- 1 hour
end
if dT>dTforget then
return true
else
return false
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Cluster Functions
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--- [Internal] Paint picture of the battle field. Does Cluster analysis and updates clusters. Sets markers if markers are enabled.
-- @param #INTEL self
function INTEL:PaintPicture()
self:F(self.lid.."Painting Picture!")
-- First remove all lost contacts from clusters.
for _,_contact in pairs(self.ContactsLost) do
local contact=_contact --#INTEL.Contact
-- Get cluster this contact belongs to (if any).
local cluster=self:GetClusterOfContact(contact)
if cluster then
self:RemoveContactFromCluster(contact, cluster)
end
end
-- Clean up cluster table.
local ClusterSet = {}
-- Now check if whole clusters were lost.
for _i,_cluster in pairs(self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
if cluster.size>0 and self:ClusterCountUnits(cluster)>0 then
-- This one has size>0 and units>0
table.insert(ClusterSet,_cluster)
else
-- This cluster is gone.
-- Remove marker.
if cluster.marker then
cluster.marker:Remove()
end
-- Marker of the arrow.
if cluster.markerID then
COORDINATE:RemoveMark(cluster.markerID)
end
-- Lost cluster.
self:LostCluster(cluster, cluster.mission)
end
end
-- Set Clusters.
self.Clusters = ClusterSet
-- Update positions.
self:_UpdateClusterPositions()
for _,_contact in pairs(self.Contacts) do
local contact=_contact --#INTEL.Contact
-- Debug info.
self:T(string.format("Paint Picture: checking for %s",contact.groupname))
-- Get the current cluster (if any) this contact belongs to.
local currentcluster=self:GetClusterOfContact(contact)
if currentcluster then
---
-- Contact is currently part of a cluster.
---
-- Check if the contact is still connected to the cluster.
local isconnected=self:IsContactConnectedToCluster(contact, currentcluster)
if isconnected then
else
--- Not connected to current cluster any more.
-- Remove from current cluster.
self:RemoveContactFromCluster(contact, currentcluster)
-- Find new cluster.
local cluster=self:_GetClosestClusterOfContact(contact)
if cluster then
-- Add contact to cluster.
self:AddContactToCluster(contact, cluster)
else
-- Create a new cluster.
local newcluster=self:_CreateClusterFromContact(contact)
-- Trigger new cluster event.
self:NewCluster(newcluster)
end
end
else
---
-- Contact is not in any cluster yet.
---
-- Debug info.
self:T(self.lid..string.format("Paint Picture: contact %s has NO current cluster", contact.groupname))
-- Get the closest existing cluster of this contact.
local cluster=self:_GetClosestClusterOfContact(contact)
if cluster then
-- Debug info.
self:T(self.lid..string.format("Paint Picture: contact %s has closest cluster #%d",contact.groupname, cluster.index))
-- Add contact to this cluster.
self:AddContactToCluster(contact, cluster)
else
-- Debug info.
self:T(self.lid..string.format("Paint Picture: contact %s has no closest cluster ==> Create new cluster", contact.groupname))
-- Create a brand new cluster.
local newcluster=self:_CreateClusterFromContact(contact)
-- Trigger event for a new cluster.
self:NewCluster(newcluster)
end
end
end
-- Update positions.
self:_UpdateClusterPositions()
-- Update F10 marker text if cluster has changed.
if self.clustermarkers then
for _,_cluster in pairs(self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
--local coordinate=self:GetClusterCoordinate(cluster)
-- Update F10 marker.
if self.verbose >= 1 then
BASE:I("Updating cluster marker and future position")
end
-- Update cluster markers.
self:UpdateClusterMarker(cluster)
-- Extrapolate future position of the cluster.
self:CalcClusterFuturePosition(cluster, 300)
end
end
return self
end
--- Create a new cluster.
-- @param #INTEL self
-- @return #INTEL.Cluster cluster The cluster.
function INTEL:_CreateCluster()
-- Create new cluster.
local cluster={} --#INTEL.Cluster
cluster.index=self.clustercounter
cluster.coordinate=COORDINATE:New(0, 0, 0)
cluster.threatlevelSum=0
cluster.threatlevelMax=0
cluster.size=0
cluster.Contacts={}
cluster.altitude=0
-- Increase counter.
self.clustercounter=self.clustercounter+1
return cluster
end
--- Create a new cluster from a first contact. The contact is automatically added to the cluster.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The first contact.
-- @return #INTEL.Cluster cluster The cluster.
function INTEL:_CreateClusterFromContact(Contact)
local cluster=self:_CreateCluster()
self:T(self.lid..string.format("Created NEW cluster #%d with first contact %s", cluster.index, Contact.groupname))
cluster.coordinate:UpdateFromCoordinate(Contact.position)
cluster.ctype=Contact.ctype
self:AddContactToCluster(Contact, cluster)
return cluster
end
--- Add cluster to table.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster to add.
function INTEL:_AddCluster(Cluster)
--TODO: Check if cluster is already in the table.
-- Add cluster.
table.insert(self.Clusters, Cluster)
return self
end
--- Add a contact to the cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @param #INTEL.Cluster cluster The cluster.
function INTEL:AddContactToCluster(contact, cluster)
if contact and cluster then
-- Add neighbour to cluster contacts.
table.insert(cluster.Contacts, contact)
-- Add to threat level sum.
cluster.threatlevelSum=cluster.threatlevelSum+contact.threatlevel
-- Increase size.
cluster.size=cluster.size+1
-- alt
self:GetClusterAltitude(cluster,true)
-- Debug info.
self:T(self.lid..string.format("Adding contact %s to cluster #%d [%s] ==> New size=%d", contact.groupname, cluster.index, cluster.ctype, cluster.size))
end
return self
end
--- Remove a contact from a cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @param #INTEL.Cluster cluster The cluster.
function INTEL:RemoveContactFromCluster(contact, cluster)
if contact and cluster then
for i=#cluster.Contacts,1,-1 do
local Contact=cluster.Contacts[i] --#INTEL.Contact
if Contact.groupname==contact.groupname then
-- Remove threat level sum.
cluster.threatlevelSum=cluster.threatlevelSum-contact.threatlevel
-- Decrease cluster size.
cluster.size=cluster.size-1
-- Remove from table.
table.remove(cluster.Contacts, i)
-- Debug info.
self:T(self.lid..string.format("Removing contact %s from cluster #%d ==> New cluster size=%d", contact.groupname, cluster.index, cluster.size))
return self
end
end
end
return self
end
--- Calculate cluster threat level sum.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Sum of all threat levels of all groups in the cluster.
function INTEL:CalcClusterThreatlevelSum(cluster)
local threatlevel=0
for _,_contact in pairs(cluster.Contacts) do
local contact=_contact --#INTEL.Contact
threatlevel=threatlevel+contact.threatlevel
end
cluster.threatlevelSum = threatlevel
return threatlevel
end
--- Calculate cluster threat level average.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Average of all threat levels of all groups in the cluster.
function INTEL:CalcClusterThreatlevelAverage(cluster)
local threatlevel=self:CalcClusterThreatlevelSum(cluster)
threatlevel=threatlevel/cluster.size
cluster.threatlevelAve = threatlevel
return threatlevel
end
--- Calculate max cluster threat level.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Max threat levels of all groups in the cluster.
function INTEL:CalcClusterThreatlevelMax(cluster)
local threatlevel=0
for _,_contact in pairs(cluster.Contacts) do
local contact=_contact --#INTEL.Contact
if contact.threatlevel>threatlevel then
threatlevel=contact.threatlevel
end
end
cluster.threatlevelMax = threatlevel
return threatlevel
end
--- Calculate cluster heading.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Heading average of all groups in the cluster.
function INTEL:CalcClusterDirection(cluster)
local direction = 0
local speedsum = 0
local n=0
for _,_contact in pairs(cluster.Contacts) do
local contact=_contact --#INTEL.Contact
if (not contact.isStatic) and contact.group:IsAlive() then
local speed = contact.group:GetVelocityKNOTS()
direction = direction + (contact.group:GetHeading()*speed)
n=n+1
speedsum = speedsum + speed
end
end
--TODO: This calculation is WRONG!
-- Simple example for two groups:
-- First group is going West, i.e. heading 090
-- Second group is going East, i.e. heading 270
-- Total is 360/2=180, i.e. South!
-- It should not go anywhere as the two movements cancel each other.
-- Apple - Correct, edge case for N=2^x, but when 2 pairs of groups drive in exact opposite directions, the cluster will split at some point?
-- maybe add the speed as weight to get a weighted factor:
if n==0 then
return 0
else
return math.floor(direction / (speedsum * n ))
end
end
--- Calculate cluster speed.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return #number Speed average of all groups in the cluster in MPS.
function INTEL:CalcClusterSpeed(cluster)
local velocity = 0 ; local n=0
for _,_contact in pairs(cluster.Contacts) do
local contact=_contact --#INTEL.Contact
if (not contact.isStatic) and contact.group:IsAlive() then
velocity = velocity + contact.group:GetVelocityMPS()
n=n+1
end
end
if n==0 then
return 0
else
return math.floor(velocity / n)
end
end
--- Calculate cluster velocity vector.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @return DCS#Vec3 Velocity vector in m/s.
function INTEL:CalcClusterVelocityVec3(cluster)
local v={x=0, y=0, z=0} --DCS#Vec3
for _,_contact in pairs(cluster.Contacts) do
local contact=_contact --#INTEL.Contact
if (not contact.isStatic) and contact.group:IsAlive() then
local vec=contact.group:GetVelocityVec3()
v.x=v.x+vec.x
v.y=v.y+vec.y
v.z=v.y+vec.z
end
end
return v
end
--- Calculate cluster future position after given seconds.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster of contacts.
-- @param #number seconds (Optional) Time interval in seconds. Default is `self.prediction`.
-- @return Core.Point#COORDINATE Calculated future position of the cluster.
function INTEL:CalcClusterFuturePosition(cluster, seconds)
-- Get current position of the cluster.
local p=self:GetClusterCoordinate(cluster)
-- Velocity vector in m/s.
local v=self:CalcClusterVelocityVec3(cluster)
-- Time in seconds.
local t=seconds or self.prediction
-- Extrapolated vec3.
local Vec3={x=p.x+v.x*t, y=p.y+v.y*t, z=p.z+v.z*t}
-- Future position.
local futureposition=COORDINATE:NewFromVec3(Vec3)
-- Create an arrow pointing in the direction of the movement.
if self.clustermarkers and self.clusterarrows then
if cluster.markerID then
COORDINATE:RemoveMark(cluster.markerID)
end
cluster.markerID = p:ArrowToAll(futureposition, self.coalition, {1,0,0}, 1, {1,1,0}, 0.5, 2, true, "Position Calc")
end
return futureposition
end
--- Check if contact is in any known cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @return #boolean If true, contact is in clusters
function INTEL:CheckContactInClusters(contact)
for _,_cluster in pairs(self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
for _,_contact in pairs(cluster.Contacts) do
local Contact=_contact --#INTEL.Contact
if Contact.groupname==contact.groupname then
return true
end
end
end
return false
end
--- Check if contact is close to any other contact this cluster.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @param #INTEL.Cluster cluster The cluster the check.
-- @return #boolean If `true`, contact is connected to this cluster.
-- @return #number Distance to cluster in meters.
function INTEL:IsContactConnectedToCluster(contact, cluster)
-- Must be of the same type. We do not want to mix aircraft with ground units.
if contact.ctype~=cluster.ctype then
return false, math.huge
end
for _,_contact in pairs(cluster.Contacts) do
local Contact=_contact --#INTEL.Contact
-- Do not calcuate the distance to the contact itself unless it is the only contact in the cluster.
if Contact.groupname~=contact.groupname or cluster.size==1 then
--local dist=Contact.position:Get2DDistance(contact.position)
local dist=Contact.position:DistanceFromPointVec2(contact.position)
-- AIR - check for spatial proximity (corrected because airprox was always false for ctype~=INTEL.Ctype.AIRCRAFT)
local airprox = true
if contact.ctype == INTEL.Ctype.AIRCRAFT then
self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,contact.altitude))
local adist = math.abs(cluster.altitude - contact.altitude)
if adist > UTILS.FeetToMeters(10000) then -- limit to 10kft
airprox = false
end
end
if dist<self.clusterradius and airprox then
return true, dist
end
end
end
return false
end
--- Check if contact is close to any contact of known clusters.
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @return #INTEL.Cluster The cluster this contact is part of or nil otherwise.
function INTEL:IsContactPartOfAnyClusters(contact)
for _,_cluster in pairs(self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
if self:IsContactConnectedToCluster(contact, cluster) then
return cluster
end
end
return nil
end
--- Get distance to cluster.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @param #INTEL.Cluster Cluster The cluster to which the distance is calculated.
-- @return #number Distance in meters.
function INTEL:_GetDistContactToCluster(Contact, Cluster)
local distmin=math.huge
for _,_contact in pairs(Cluster.Contacts) do
local contact=_contact --#INTEL.Contact
if contact.group and contact.group:IsAlive() and Contact.groupname~=contact.groupname then
local dist=Contact.position:Get2DDistance(contact.position)
if dist<distmin then
distmin=dist
end
end
end
return distmin
end
--- Get closest cluster of contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #INTEL.Cluster The cluster this contact is part of or `#nil` otherwise.
-- @return #number Distance to cluster in meters.
function INTEL:_GetClosestClusterOfContact(Contact)
local Cluster=nil --#INTEL.Cluster
local distmin=self.clusterradius
if not Contact.altitude then
Contact.altitude = Contact.group:GetAltitude()
end
for _,_cluster in pairs(self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
if cluster.ctype==Contact.ctype then
local dist=self:_GetDistContactToCluster(Contact, cluster)
-- AIR - check for spatial proximity (ff: Changed because airprox was always false for ctype~=AIRCRAFT!)
local airprox=true
if Contact.ctype == INTEL.Ctype.AIRCRAFT then
if not cluster.altitude then
cluster.altitude = self:GetClusterAltitude(cluster,true)
end
local adist = math.abs(cluster.altitude - Contact.altitude)
self:T(string.format("Cluster Alt=%d | Contact Alt=%d",cluster.altitude,Contact.altitude))
if adist > UTILS.FeetToMeters(10000) then
airprox = false
end
end
if dist<distmin and airprox then
Cluster=cluster
distmin=dist
end
end
end
return Cluster, distmin
end
--- Get the cluster this contact belongs to (if any).
-- @param #INTEL self
-- @param #INTEL.Contact contact The contact.
-- @return #INTEL.Cluster The cluster this contact belongs to or nil.
function INTEL:GetClusterOfContact(contact)
for _,_cluster in pairs(self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
for _,_contact in pairs(cluster.Contacts) do
local Contact=_contact --#INTEL.Contact
if Contact.groupname==contact.groupname then
return cluster
end
end
end
return nil
end
--- Get the altitude of a cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #boolean Update If `true`, update the altitude. Default is to just return the last stored altitude.
-- @return #number The average altitude (ASL) of this cluster in meters.
function INTEL:GetClusterAltitude(Cluster, Update)
-- Init.
local newalt = 0
local n=0
-- Loop over all contacts.
for _,_contact in pairs(Cluster.Contacts) do
local contact=_contact --#INTEL.Contact
if contact.altitude then
newalt = newalt + contact.altitude
n=n+1
end
end
-- Average.
local avgalt = 0
if n>0 then
avgalt = newalt/n
end
-- Update cluster coordinate.
Cluster.altitude = avgalt
self:T(string.format("Updating Cluster Altitude: %d",Cluster.altitude))
return Cluster.altitude
end
--- Get the coordinate of a cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #boolean Update (Optional) If `true`, update the coordinate. Default is to just return the last stored position.
-- @return Core.Point#COORDINATE The coordinate of this cluster.
function INTEL:GetClusterCoordinate(Cluster, Update)
-- Init.
local x=0 ; local y=0 ; local z=0 ; local n=0
-- Loop over all contacts.
for _,_contact in pairs(Cluster.Contacts) do
local contact=_contact --#INTEL.Contact
local vec3=nil --DCS#Vec3
if Update and contact.group and contact.group:IsAlive() then
vec3 = contact.group:GetVec3()
end
if not vec3 then
vec3=contact.position
end
if vec3 then
-- Sum up posits.
x=x+vec3.x
y=y+vec3.y
z=z+vec3.z
-- Increase counter.
n=n+1
end
end
if n>0 then
-- Average.
local Vec3={x=x/n, y=y/n, z=z/n} --DCS#Vec3
-- Update cluster coordinate.
Cluster.coordinate:UpdateFromVec3(Vec3)
end
return Cluster.coordinate
end
--- Check if the coordindate of the cluster changed.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @param #number (Optional) Threshold in meters. Default 100 m.
-- @param Core.Point#COORDINATE Coordinate (Optional) Reference coordinate. Default is the last known coordinate of the cluster.
-- @return #boolean If `true`, the coordinate changed by more than the given threshold.
function INTEL:_CheckClusterCoordinateChanged(Cluster, Coordinate, Threshold)
Threshold=Threshold or 100
Coordinate=Coordinate or Cluster.coordinate
-- Positions of cluster.
local a=Coordinate:GetVec3()
local b=self:GetClusterCoordinate(Cluster, true):GetVec3()
local dist=UTILS.VecDist3D(a,b)
if dist>Threshold then
return true
else
return false
end
end
--- Update coordinates of the known clusters.
-- @param #INTEL self
function INTEL:_UpdateClusterPositions()
for _,_cluster in pairs (self.Clusters) do
local cluster=_cluster --#INTEL.Cluster
-- Update cluster coordinate.
local coord = self:GetClusterCoordinate(cluster, true)
local alt = self:GetClusterAltitude(cluster,true)
-- Debug info.
self:T(self.lid..string.format("Updating Cluster position size: %s", cluster.size))
end
return self
end
--- Count number of alive units in contact.
-- @param #INTEL self
-- @param #INTEL.Contact Contact The contact.
-- @return #number unitcount
function INTEL:ContactCountUnits(Contact)
if Contact.isStatic then
if Contact.group and Contact.group:IsAlive() then
return 1
else
return 0
end
else
if Contact.group then
local n=Contact.group:CountAliveUnits()
return n
else
return 0
end
end
end
--- Count number of alive units in cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster
-- @return #number unitcount
function INTEL:ClusterCountUnits(Cluster)
local unitcount = 0
for _,_contact in pairs (Cluster.Contacts) do
local contact=_contact --#INTEL.Contact
unitcount = unitcount + self:ContactCountUnits(contact)
end
return unitcount
end
--- Update cluster F10 marker.
-- @param #INTEL self
-- @param #INTEL.Cluster cluster The cluster.
-- @return #INTEL self
function INTEL:UpdateClusterMarker(cluster)
-- Create a marker.
local unitcount = self:ClusterCountUnits(cluster)
local text=string.format("Cluster #%d: %s\nSize %d\nUnits %d\nTLsum=%d", cluster.index, cluster.ctype, cluster.size, unitcount, cluster.threatlevelSum)
if not cluster.marker then
-- First time ==> need to create a new marker object.
cluster.marker=MARKER:New(cluster.coordinate, text):ToCoalition(self.coalition)
else
-- Need to refresh?
local refresh=false
-- Check if marker text changed.
if cluster.marker.text~=text then
cluster.marker.text=text
refresh=true
end
-- Check if coordinate changed.
local coordchange=self:_CheckClusterCoordinateChanged(cluster, cluster.marker.coordinate)
if coordchange then
cluster.marker.coordinate:UpdateFromCoordinate(cluster.coordinate)
refresh=true
end
if refresh then
cluster.marker:Refresh()
end
end
return self
end
--- Get the contact with the highest threat level from the cluster.
-- @param #INTEL self
-- @param #INTEL.Cluster Cluster The cluster.
-- @return #INTEL.Contact the contact or nil if none
function INTEL:GetHighestThreatContact(Cluster)
local threatlevel=-1
local rcontact = nil
for _,_contact in pairs(Cluster.Contacts) do
local contact=_contact --Ops.Intel#INTEL.Contact
if contact.threatlevel>threatlevel then
threatlevel=contact.threatlevel
rcontact = contact
end
end
return rcontact
end
--- Enable 70/80s era pulse-Doppler ground-clutter simulation.
-- Only affects contacts detected via radar (DetectRadar=true paths).
-- Has no effect on visual, optical, IRST, RWR or datalink detections.
-- @param #INTEL self
-- @param #number MinAltAGL Min AGL altitude in metres for reliable detection.
-- Below this the detection probability drops linearly.
-- Default 500 m (≈ clutter floor for P-18 / P-37).
-- @param #number NotchHalfDeg Half-width of the velocity notch in degrees.
-- Targets with radial-velocity fraction < sin(NotchHalf)
-- are suppressed. Default 15° (≈ P-18 / Bar Lock spec).
-- @param #number MinSpeedMps Minimum speed in m/s that the MTI filter can track.
-- Default 50 m/s (≈ 100 kt).
-- @param #number RadarRangeKm Nominal detection range in km for the reference aircraft
-- (RCS_Reference, default 5 m²). Used only for RCS range
-- scaling; has no effect when DopplerRCS is false.
-- Default 200 km (≈ P-37 instrumented range vs fighter).
-- @param #boolean RCS If false, disable RCS range scaling (keep AC only).
-- Default true.
-- @return #INTEL self
function INTEL:SetDopplerRadar(MinAltAGL, NotchHalfDeg, MinSpeedMps, RadarRangeKm, RCS)
self.DopplerRadar = true
self.DopplerMinAltAGL = MinAltAGL or 500
self.DopplerNotchSin = math.sin(math.rad(NotchHalfDeg or 15))
self.DopplerMinSpeedMps = MinSpeedMps or 50
self.DopplerRCS = (RCS ~= false) -- default true
self.DopplerRadarRangeM = (RadarRangeKm or 200) * 1000
return self
end
--- Disable Doppler radar simulation.
-- @param #INTEL self
-- @return #INTEL self
function INTEL:SetDopplerRadarOff()
self.DopplerRadar = false
return self
end
--- Override the per-type RCS value for a DCS unit type name.
-- Useful for modded aircraft or mission-specific tweaks.
-- @param #INTEL self
-- @param #string TypeName DCS unit type name (e.g. "MiG-29A")
-- @param #number RCS_m2 Side-on RCS in m²
-- @return #INTEL self
function INTEL:SetTypeRCS(TypeName, RCS_m2)
INTEL.RCS_Table[TypeName] = RCS_m2
return self
end
--- (Internal) Compute the aspect-weighted RCS for a target unit as seen
-- from a given radar position.
--
-- The model blends the side-on (maximum) and nose/tail-on (minimum) RCS
-- using the geometry of the target's velocity relative to the radar line:
--
-- σ_eff = σ_base × ( f_nose + (1 f_nose) × sin²(aspect_from_radial) )
--
-- where aspect_from_radial is 0° when the target flies toward/away from
-- the radar (nose-on) and 90° when the target crosses the beam (side-on).
--
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT TargetUnit
-- @param #table rpos Radar position as Vec3 {x,y,z}
-- @param #number spd Target speed in m/s (pre-computed for efficiency)
-- @param DCS#Vec3 tvel Target velocity vector (pre-computed)
-- @return #number Effective RCS in m²
function INTEL:_GetAspectRCS(TargetUnit, rpos, spd, tvel)
-- Look up base (side-on) RCS
local typename = TargetUnit:GetTypeName()
local base_rcs = INTEL.RCS_Table[typename]
if not base_rcs then
-- Fallback: category default
local cat = TargetUnit:GetGroup() and TargetUnit:GetGroup():GetCategory()
base_rcs = (cat and INTEL.RCS_CategoryDefault[cat]) or INTEL.RCS_Reference
end
-- Aspect-dependent factor
if spd < 1 then return base_rcs end
local tpos = TargetUnit:GetVec3()
local dx = rpos.x - tpos.x -- vector target → radar (horizontal)
local dz = rpos.z - tpos.z
local d = math.sqrt(dx * dx + dz * dz)
if d < 1 then return base_rcs end
-- cos of angle between target velocity and target→radar line
-- = 1: nose/tail directly toward radar; = 0: pure crossing (beam)
local cos_a = (tvel.x * dx + tvel.z * dz) / (spd * d)
-- sin²(aspect_from_radial) = 1 cos² ; gives 0 nose-on, 1 beam-on
local sin2_a = 1.0 - cos_a * cos_a
local f = INTEL.RCS_NoseOnFraction
return base_rcs * (f + (1.0 - f) * sin2_a)
end
--- (Internal) Check whether a target unit would be detected by a 70/80s
-- pulse-Doppler radar located at the given radar unit position.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT TargetUnit
-- @param Wrapper.Unit#UNIT RadarUnit
-- @return #boolean true = detected
-- @return #string rejection reason: "speed" | "clutter" | "notch" | "rcs"
function INTEL:_CheckDopplerDetection(TargetUnit, RadarUnit)
-- Pre-compute common geometry (shared by notch + RCS checks)
local spd = TargetUnit:GetVelocityMPS()
local rpos = RadarUnit:GetVec3()
local tpos = TargetUnit:GetVec3()
local tvel = TargetUnit:GetVelocity()
local dx = tpos.x - rpos.x
local dz = tpos.z - rpos.z
local slant = math.sqrt(dx * dx + dz * dz) -- 2-D slant range in metres
-- ── A. Minimum speed gate ──────────────────────────────────
if spd < self.DopplerMinSpeedMps then
return false, "speed"
end
-- ── B. AGL ground-clutter rejection ───────────────────────
local agl = TargetUnit:GetAltitude(true) -- metres AGL
if agl < self.DopplerMinAltAGL then
-- P(detect) rises linearly from 0 at deck to 1 at DopplerMinAltAGL
if math.random() > (agl / self.DopplerMinAltAGL) then
return false, "clutter"
end
end
-- ── C. Velocity notch ─────────────────────────────────────
if slant > 1 then
local nx = dx / slant
local nz = dz / slant
local vr = tvel.x * nx + tvel.z * nz -- radial velocity (m/s)
local vr_frac = math.abs(vr) / math.max(spd, 1)
if vr_frac < self.DopplerNotchSin then
return false, "notch"
end
end
-- ── D. RCS-based range scaling ─────────────────────────────
-- R_max ∝ σ^0.25 (from the radar range equation).
-- Effective detection range = DopplerRadarRangeM × (σ_eff / σ_ref)^0.25
-- Beyond that range: target not detected (hard cutoff at 100%; soft fade
-- starts at 80% of R_max to smooth the transition).
if self.DopplerRCS and slant > 1 then
local sigma = self:_GetAspectRCS(TargetUnit, rpos, spd, tvel)
-- (σ/σ_ref)^0.25 — clamp to avoid log of 0 for VLO aircraft
local scale = (sigma / INTEL.RCS_Reference) ^ 0.25
local R_max = self.DopplerRadarRangeM * scale
if slant > R_max then
return false, "rcs"
end
-- Soft fade zone: linear probability drop from 1 at 80% R_max to 0 at R_max
local fade_start = R_max * 0.80
if slant > fade_start then
local p = (R_max - slant) / (R_max - fade_start) -- 1→0
if math.random() > p then
return false, "rcs"
end
end
end
return true
end
---(Internal) Return the detected target groups of the controllable as a table.
-- We wrap the original function so the Doppler post-filter is transparent:
-- the existing RadarBlur / RadarAcceptRange logic is unchanged, and the
-- Doppler check runs once after all units have been collected.
-- The optional parameters specify the detection methods that can be applied.
-- If no detection method is given, the detection will use all the available methods by default.
-- @param #INTEL self
-- @param Wrapper.Unit#UNIT Unit The unit detecting.
-- @param #table DetectedUnits Table of detected units to be filled.
-- @param #table RecceDetecting Table of recce per unit to be filled.
-- @param #boolean DetectVisual (Optional) If *false*, do not include visually detected targets.
-- @param #boolean DetectOptical (Optional) If *false*, do not include optically detected targets.
-- @param #boolean DetectRadar (Optional) If *false*, do not include targets detected by radar.
-- @param #boolean DetectIRST (Optional) If *false*, do not include targets detected by IRST.
-- @param #boolean DetectRWR (Optional) If *false*, do not include targets detected by RWR.
-- @param #boolean DetectDLINK (Optional) If *false*, do not include targets detected by data link.
function INTEL:GetDetectedUnitsDoppler(Unit, DetectedUnits, RecceDetecting,
DetectVisual, DetectOptical, DetectRadar,
DetectIRST, DetectRWR, DetectDLINK)
-- Run the original detection
self:GetDetectedUnits(Unit,DetectedUnits,RecceDetecting,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK)(self, Unit, DetectedUnits, RecceDetecting,
DetectVisual, DetectOptical, DetectRadar,
DetectIRST, DetectRWR, DetectDLINK)
-- Apply Doppler post-filter only when radar channel is active
if not self.DopplerRadar then return end
if DetectRadar == false then return end
local remove = {}
for name, unit in pairs(DetectedUnits) do
-- Only filter live UNIT objects (not STATICs) that are airborne
if unit:IsInstanceOf("UNIT") and unit:IsAir() then
local ok, reason = self:_CheckDopplerDetection(unit, Unit)
if not ok then
table.insert(remove, name)
if self.verbose and self.verbose >= 2 then
self:T(string.format(
"%sDoppler: suppressed %s [%s] by %s",
self.lid, name, reason, Unit:GetName()))
end
end
end
end
for _, name in ipairs(remove) do
DetectedUnits[name] = nil
RecceDetecting[name] = nil
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------
-- TODO Start INTEL_DLINK
----------------------------------------------------------------------------------------------
--- **Ops_DLink** - Support for Office of Military Intelligence.
--
-- **Main Features:**
--
-- * Overcome limitations of (non-available) datalinks between ground radars
-- * Detect and track contacts consistently across INTEL instances
-- * Use FSM events to link functionality into your scripts
-- * Easy setup
--
--- ===
--
-- ### Author: **applevangelist**
--- INTEL_DLINK class.
-- @type INTEL_DLINK
-- @field #string ClassName Name of the class.
-- @field #string lid Class id string for output to DCS log file.
-- @field #number verbose Make the logging verbose.
-- @field #string alias Alias name for logging.
-- @field #number cachetime Number of seconds to keep an object.
-- @field #number interval Number of seconds between collection runs.
-- @field #table contacts Table of Ops.Intel#INTEL.Contact contacts.
-- @field #table clusters Table of Ops.Intel#INTEL.Cluster clusters.
-- @field #table contactcoords Table of contacts' Core.Point#COORDINATE objects.
-- @extends Core.Fsm#FSM
--- INTEL_DLINK data aggregator
-- @field #INTEL_DLINK
INTEL_DLINK = {
ClassName = "INTEL_DLINK",
verbose = 0,
lid = nil,
alias = nil,
cachetime = 120,
interval = 20,
contacts = {},
clusters = {},
contactcoords = {},
}
--- Version string
-- @field #string version
INTEL_DLINK.version = "0.0.2"
--- Function to instantiate a new object
-- @param #INTEL_DLINK self
-- @param #table Intels Table of Ops.Intel#INTEL objects.
-- @param #string Alias (optional) Name of this instance. Default "SPECTRE"
-- @param #number Interval (optional) When to query #INTEL objects for detected items (default 20 seconds).
-- @param #number Cachetime (optional) How long to cache detected items (default 300 seconds).
-- @usage Use #INTEL_DLINK if you want to merge data from a number of #INTEL objects into one. This might be useful to simulate a
-- Data Link, e.g. for Russian-tech based EWR, realising a Star Topology @{https://en.wikipedia.org/wiki/Network_topology#Star}
-- in a basic setup. It will collect the contacts and clusters from the #INTEL objects.
-- Contact duplicates are removed. Clusters might contain duplicates (Might fix that later, WIP).
--
-- Basic setup:
--
-- local datalink = INTEL_DLINK:New({myintel1,myintel2}), "FSB", 20, 300)
-- datalink:__Start(2)
--
-- Add an Intel while running:
--
-- datalink:AddIntel(myintel3)
--
-- Gather the data:
--
-- datalink:GetContactTable() -- #table of #INTEL.Contact contacts.
-- datalink:GetClusterTable() -- #table of #INTEL.Cluster clusters.
-- datalink:GetDetectedItemCoordinates() -- #table of contact coordinates, to be compatible with @{Functional.Detection#DETECTION}.
--
-- Gather data with the event function:
--
-- function datalink:OnAfterCollected(From, Event, To, Contacts, Clusters)
-- ... <your code here> ...
-- end
--
function INTEL_DLINK:New(Intels, Alias, Interval, Cachetime)
-- Inherit everything from FSM class.
local self=BASE:Inherit(self, FSM:New()) -- #INTEL_DLINK
self.intels = Intels or {}
self.contacts = {}
self.clusters = {}
self.contactcoords = {}
-- Set alias.
if Alias then
self.alias=tostring(Alias)
else
self.alias="SPECTRE"
end
-- Interval
self.interval = Interval or 20
-- Set some string id for output to DCS.log file.
self.lid=string.format("INTEL_DLINK %s | ", self.alias)
-- Cache time
self:SetDLinkCacheTime(Cachetime or 120)
-- Start State.
self:SetStartState("Stopped")
-- Add FSM transitions.
-- From State --> Event --> To State
self:AddTransition("Stopped", "Start", "Running") -- Start FSM.
self:AddTransition("*", "Collect", "*") -- Collect data.
self:AddTransition("*", "Collected", "*") -- Collection of data done.
self:AddTransition("*", "Stop", "Stopped") -- Stop FSM.
----------------------------------------------------------------------------------------------
-- Pseudo Functions
----------------------------------------------------------------------------------------------
--- Triggers the FSM event "Start". Starts the INTEL_DLINK.
-- @function [parent=#INTEL_DLINK] Start
-- @param #INTEL_DLINK self
--- Triggers the FSM event "Start" after a delay. Starts the INTEL_DLINK.
-- @function [parent=#INTEL_DLINK] __Start
-- @param #INTEL_DLINK self
-- @param #number delay Delay in seconds.
--- Triggers the FSM event "Stop". Stops the INTEL_DLINK.
-- @param #INTEL_DLINK self
--- Triggers the FSM event "Stop" after a delay. Stops the INTEL_DLINK.
-- @function [parent=#INTEL_DLINK] __Stop
-- @param #INTEL_DLINK self
-- @param #number delay Delay in seconds.
--- Triggers the FSM event "Collect". Used internally to collect all data.
-- @function [parent=#INTEL_DLINK] Collect
-- @param #INTEL_DLINK self
--- Triggers the FSM event "Collect" after a delay.
-- @function [parent=#INTEL_DLINK] __Status
-- @param #INTEL_DLINK self
-- @param #number delay Delay in seconds.
--- On After "Collected" event. Data tables have been refreshed.
-- @function [parent=#INTEL_DLINK] OnAfterCollected
-- @param #INTEL_DLINK self
-- @param #string From From state.
-- @param #string Event Event.
-- @param #string To To state.
-- @param #table Contacts Table of #INTEL.Contact Contacts.
-- @param #table Clusters Table of #INTEL.Cluster Clusters.
return self
end
----------------------------------------------------------------------------------------------
-- Helper & User Functions
----------------------------------------------------------------------------------------------
--- Function to add an #INTEL object to the aggregator
-- @param #INTEL_DLINK self
-- @param Ops.Intel#INTEL Intel the #INTEL object to add
-- @return #INTEL_DLINK self
function INTEL_DLINK:AddIntel(Intel)
self:T(self.lid .. "AddIntel")
if Intel then
table.insert(self.intels,Intel)
end
return self
end
----------------------------------------------------------------------------------------------
-- FSM Functions
----------------------------------------------------------------------------------------------
--- Function to start the work.
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @return #INTEL_DLINK self
function INTEL_DLINK:onafterStart(From, Event, To)
self:T({From, Event, To})
local text = string.format("Version %s started.", self.version)
self:I(self.lid .. text)
self:__Collect(-math.random(1,10))
return self
end
--- Function to set how long INTEL DLINK remembers contacts.
-- @param #INTEL_DLINK self
-- @param #number seconds (Optional) Remember this many seconds. Defaults to 120.
-- @return #INTEL_DLINK self
function INTEL_DLINK:SetDLinkCacheTime(seconds)
self.cachetime = math.abs(seconds or 120)
self:I(self.lid.."Caching for "..self.cachetime.." seconds.")
return self
end
--- Function to collect data from the various #INTEL
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @return #INTEL_DLINK self
function INTEL_DLINK:onbeforeCollect(From, Event, To)
self:T({From, Event, To})
-- run through our #INTEL objects and gather the contacts tables
self:T("Contacts Data Gathering")
local newcontacts = {}
local intels = self.intels -- #table
for _,_intel in pairs (intels) do
_intel = _intel -- #INTEL
if _intel:Is("Running") then
local ctable = _intel:GetContactTable() or {} -- #INTEL.Contact
for _,_contact in pairs (ctable) do
local _ID = string.format("%s-%d",_contact.groupname, _contact.Tdetected)
self:T(string.format("Adding %s",_ID))
newcontacts[_ID] = _contact
end
end
end
-- clean up for stale contacts and dupes
self:T("Cleanup")
local contacttable = {}
local coordtable = {}
local TNow = timer.getAbsTime()
local Tcache = self.cachetime
for _ind, _contact in pairs(newcontacts) do -- #string, #INTEL.Contact
if TNow - _contact.Tdetected < Tcache then
if (not contacttable[_contact.groupname]) or (contacttable[_contact.groupname] and contacttable[_contact.groupname].Tdetected < _contact.Tdetected) then
self:T(string.format("Adding %s",_contact.groupname))
contacttable[_contact.groupname] = _contact
table.insert(coordtable,_contact.position)
end
end
end
-- run through our #INTEL objects and gather the clusters tables
self:T("Clusters Data Gathering")
local newclusters = {}
local intels = self.intels -- #table
for _,_intel in pairs (intels) do
_intel = _intel -- #INTEL
if _intel:Is("Running") then
local ctable = _intel:GetClusterTable() or {} -- #INTEL.Cluster
for _,_cluster in pairs (ctable) do
local _ID = string.format("%s-%d", _intel.alias, _cluster.index)
self:T(string.format("Adding %s",_ID))
table.insert(newclusters,_cluster)
end
end
end
-- update self tables
self.contacts = contacttable
self.contactcoords = coordtable
self.clusters = newclusters
self:__Collected(1, contacttable, newclusters) -- make table available via FSM Event
-- schedule next round
local interv = self.interval * -1
self:__Collect(interv)
return self
end
--- Function called after collection is done
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @param #table Contacts The table of collected #INTEL.Contact contacts
-- @param #table Clusters The table of collected #INTEL.Cluster clusters
-- @return #INTEL_DLINK self
function INTEL_DLINK:onbeforeCollected(From, Event, To, Contacts, Clusters)
self:T({From, Event, To})
return self
end
--- Function to stop
-- @param #INTEL_DLINK self
-- @param #string From The From state
-- @param #string Event The Event triggering this call
-- @param #string To The To state
-- @return #INTEL_DLINK self
function INTEL_DLINK:onafterStop(From, Event, To)
self:T({From, Event, To})
local text = string.format("Version %s stopped.", self.version)
self:I(self.lid .. text)
return self
end
--- Function to query the detected contacts
-- @param #INTEL_DLINK self
-- @return #table Table of #INTEL.Contact contacts
function INTEL_DLINK:GetContactTable()
self:T(self.lid .. "GetContactTable")
return self.contacts
end
--- Function to query the detected clusters
-- @param #INTEL_DLINK self
-- @return #table Table of #INTEL.Cluster clusters
function INTEL_DLINK:GetClusterTable()
self:T(self.lid .. "GetClusterTable")
return self.clusters
end
--- Function to query the detected contact coordinates
-- @param #INTEL_DLINK self
-- @return #table Table of the contacts' Core.Point#COORDINATE objects.
function INTEL_DLINK:GetDetectedItemCoordinates()
self:T(self.lid .. "GetDetectedItemCoordinates")
return self.contactcoords
end
----------------------------------------------------------------------------------------------
-- End INTEL_DLINK
----------------------------------------------------------------------------------------------