mirror of
https://github.com/FlightControl-Master/MOOSE.git
synced 2026-08-04 23:09:05 +00:00
2a73d8d34a
Add a new passive TERRITORY functional module (Functional/Territory.lua) and register it in Modules.lua. TERRITORY provides zone-backed territory objects and DB helpers (Find/Add/Delete). Update AUFTRAG: bump version to 1.5.0, introduce AUFTRAG.Summary, add an "Evaluated" event/transition, populate and emit mission summary from Evaluate(), set a dTevaluate for BAI, replace Stop() with __Stop(-120) on terminal states, and clear summary on repeats/stops. Minor ARTY debug prints and make Speed param optional. INTEL: add _UpdateAgents and SetAgentAuto to optionally auto-refresh the detection set and invoke it during status updates. OPSGROUP: require StayInZoneTime>0 before treating task as stayed. AIRBASE: determine DCS object category and name, handle ship/oil-rig/helipad edge cases (add static helipad to DB), keep Zell detection, and create parkingCircle with explicit parameter. Coherent small refactors and safety checks across modules.
3191 lines
111 KiB
Lua
3191 lines
111 KiB
Lua
--- **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 = false,
|
||
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 ≈ ±12–18° 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 Set
|
||
-- 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.10–0.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
|
||
-- @return #INTEL self
|
||
function INTEL:_UpdateAgents()
|
||
|
||
-- Filter coalition.
|
||
if self.coalition then
|
||
local coalitionname=UTILS.GetCoalitionName(self.coalition):lower()
|
||
self.detectionset:FilterCoalitions(coalitionname):FilterAlive():FilterOnce()
|
||
end
|
||
|
||
end
|
||
|
||
--- 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()
|
||
self.DopplerRadar = false
|
||
|
||
------------------------
|
||
--- 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
|
||
|
||
--- Set whether the detection set, aka agents, are updated periodically. With this all alive groups of the INTEL coalition are included.
|
||
-- @param #INTEL self
|
||
-- @param #boolean switch If `true` or nil, all groups of the coalition will be added automatically to the Agent set.
|
||
-- @return #INTEL self
|
||
function INTEL:SetAgentAuto(switch)
|
||
if switch==nil then
|
||
switch=true
|
||
else
|
||
switch=false
|
||
end
|
||
self.update_detectionset=switch
|
||
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()
|
||
|
||
if self.update_detectionset then
|
||
self:_UpdateAgents()
|
||
end
|
||
|
||
-- 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 == true 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
|
||
local typename = Contact.group:GetTypeName()
|
||
local base_rcs = INTEL.RCS_Table[typename]
|
||
|
||
if not base_rcs then
|
||
-- Fallback: category default
|
||
local cat = Contact.group:GetCategory()
|
||
base_rcs = (cat and INTEL.RCS_CategoryDefault[cat]) or INTEL.RCS_Reference
|
||
end
|
||
Contact.rcs = base_rcs
|
||
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 A–C only).
|
||
-- Default true.
|
||
-- @return #INTEL self
|
||
function INTEL:SetDopplerRadar(MinAltAGL, NotchHalfDeg, MinSpeedMps, RadarRangeKm, RCS)
|
||
self:T(self.lid .. "SetDopplerRadar")
|
||
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:T(self.lid .. "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)
|
||
self:T(self.lid .. "SetTypeRCS")
|
||
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)
|
||
self:T(self.lid .. "_GetAspectRCS")
|
||
-- 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)
|
||
self:T(self.lid .. "_CheckDopplerDetection")
|
||
-- Pre-compute common geometry (shared by notch + RCS checks)
|
||
local spd = TargetUnit:GetVelocityMPS()
|
||
local rpos = RadarUnit:GetVec3()
|
||
local tpos = TargetUnit:GetVec3()
|
||
local tvel = TargetUnit:GetVelocityVec3()
|
||
|
||
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 == true 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)
|
||
self:T(self.lid .. "GetDetectedUnitsDoppler")
|
||
-- Run the original detection
|
||
self:GetDetectedUnits(Unit,DetectedUnits,RecceDetecting,DetectVisual,DetectOptical,DetectRadar,DetectIRST,DetectRWR,DetectDLINK)
|
||
|
||
-- Apply Doppler post-filter only when radar channel is active
|
||
if self.DopplerRadar == false 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:T(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:T(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
|
||
----------------------------------------------------------------------------------------------
|