From b0eef34146edb5d76020698389a21a4e97fcd59c Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 3 Jan 2023 10:14:34 +0100 Subject: [PATCH 1/2] #CONTROLLABLE * Docu fix #WAREHOUSE * Changes from dev #ZONE * Additions to ZONE_POLYGON --- Moose Development/Moose/Core/Zone.lua | 72 ++++++ .../Moose/Functional/Warehouse.lua | 236 +++++++++++------- .../Moose/Wrapper/Controllable.lua | 2 +- 3 files changed, 215 insertions(+), 95 deletions(-) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 5f89fdaed..70495842d 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -2084,6 +2084,52 @@ function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlph return self end + +--- Get the smallest circular zone encompassing all points points of the polygon zone. +-- @param #ZONE_POLYGON_BASE self +-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. +-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. +-- @return #ZONE_RADIUS The circular zone. +function ZONE_POLYGON_BASE:GetZoneRadius(ZoneName, DoNotRegisterZone) + + local center=self:GetVec2() + + local radius=0 + + for _,_vec2 in pairs(self._.Polygon) do + local vec2=_vec2 --DCS#Vec2 + + local r=UTILS.VecDist2D(center, vec2) + + if r>radius then + radius=r + end + + end + + local zone=ZONE_RADIUS:New(ZoneName or self.ZoneName, center, radius, DoNotRegisterZone) + + return zone +end + + +--- Get the smallest rectangular zone encompassing all points points of the polygon zone. +-- @param #ZONE_POLYGON_BASE self +-- @param #string ZoneName (Optional) Name of the zone. Default is the name of the polygon zone. +-- @param #boolean DoNotRegisterZone (Optional) If `true`, zone is not registered. +-- @return #ZONE_POLYGON The rectangular zone. +function ZONE_POLYGON_BASE:GetZoneQuad(ZoneName, DoNotRegisterZone) + + local vec1, vec3=self:GetBoundingVec2() + + local vec2={x=vec1.x, y=vec3.y} + local vec4={x=vec3.x, y=vec1.y} + + local zone=ZONE_POLYGON_BASE:New(ZoneName or self.ZoneName, {vec1, vec2, vec3, vec4}) + + return zone +end + --- Smokes the zone boundaries in a color. -- @param #ZONE_POLYGON_BASE self -- @param Utilities.Utils#SMOKECOLOR SmokeColor The smoke color. @@ -2286,6 +2332,32 @@ function ZONE_POLYGON_BASE:GetBoundingSquare() return { x1 = x1, y1 = y1, x2 = x2, y2 = y2 } end +--- Get the bounding 2D vectors of the polygon. +-- @param #ZONE_POLYGON_BASE self +-- @return DCS#Vec2 Coordinates of western-southern-lower vertex of the box. +-- @return DCS#Vec2 Coordinates of eastern-northern-upper vertex of the box. +function ZONE_POLYGON_BASE:GetBoundingVec2() + + local x1 = self._.Polygon[1].x + local y1 = self._.Polygon[1].y + local x2 = self._.Polygon[1].x + local y2 = self._.Polygon[1].y + + for i = 2, #self._.Polygon do + self:T2( { self._.Polygon[i], x1, y1, x2, y2 } ) + x1 = ( x1 > self._.Polygon[i].x ) and self._.Polygon[i].x or x1 + x2 = ( x2 < self._.Polygon[i].x ) and self._.Polygon[i].x or x2 + y1 = ( y1 > self._.Polygon[i].y ) and self._.Polygon[i].y or y1 + y2 = ( y2 < self._.Polygon[i].y ) and self._.Polygon[i].y or y2 + + end + + local vec1={x=x1, y=y1} + local vec2={x=x2, y=y2} + + return vec1, vec2 +end + --- Draw a frontier on the F10 map with small filled circles. -- @param #ZONE_POLYGON_BASE self -- @param #number Coalition (Optional) Coalition: All=-1, Neutral=0, Red=1, Blue=2. Default -1= All. diff --git a/Moose Development/Moose/Functional/Warehouse.lua b/Moose Development/Moose/Functional/Warehouse.lua index 26cef4db9..8a1dfcd95 100644 --- a/Moose Development/Moose/Functional/Warehouse.lua +++ b/Moose Development/Moose/Functional/Warehouse.lua @@ -302,8 +302,8 @@ -- -- Initial Spawn states is as follows: -- GROUND: ROE, "Return Fire" Alarm, "Green" --- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" --- NAVAL ROE, "Return Fire" Alarm,"N/A" +-- AIR: ROE, "Return Fire" Reaction to Threat, "Passive Defense" +-- NAVAL ROE, "Return Fire" Alarm,"N/A" -- -- A request can be added by the @{#WAREHOUSE.AddRequest}(*warehouse*, *AssetDescriptor*, *AssetDescriptorValue*, *nAsset*, *TransportType*, *nTransport*, *Prio*, *Assignment*) function. -- The parameters are @@ -2647,6 +2647,13 @@ function WAREHOUSE:SetWarehouseZone(zone) return self end +--- Get the warehouse zone. +-- @param #WAREHOUSE self +-- @return Core.Zone#ZONE The warehouse zone. +function WAREHOUSE:GetWarehouseZone() + return self.zone +end + --- Set auto defence on. When the warehouse is under attack, all ground assets are spawned automatically and will defend the warehouse zone. -- @param #WAREHOUSE self -- @return #WAREHOUSE self @@ -5810,6 +5817,7 @@ function WAREHOUSE:_SpawnAssetRequest(Request) -- Now we try to find all parking spots for all cargo groups in advance. Due to the for loop, the parking spots do not get updated while spawning. local Parking={} if Request.cargocategory==Group.Category.AIRPLANE or Request.cargocategory==Group.Category.HELICOPTER then + --TODO: Check for airstart. Should be a request property. Parking=self:_FindParkingForAssets(self.airbase, cargoassets) or {} end @@ -6069,7 +6077,9 @@ function WAREHOUSE:_SpawnAssetAircraft(alias, asset, request, parking, uncontrol end if self.Debug then - coord:MarkToAll(string.format("Spawnplace unit %s terminal %d.", unit.name, terminal)) + local text=string.format("Spawnplace unit %s terminal %d.", unit.name, terminal) + coord:MarkToAll(text) + env.info(text) end unit.x=coord.x @@ -7374,6 +7384,7 @@ function WAREHOUSE:_CheckRequestNow(request) local _transports local _assetattribute local _assetcategory + local _assetairstart=false -- Check if at least one (cargo) asset is available. if _nassets>0 then @@ -7381,21 +7392,28 @@ function WAREHOUSE:_CheckRequestNow(request) -- Get the attibute of the requested asset. _assetattribute=_assets[1].attribute _assetcategory=_assets[1].category + _assetairstart=_assets[1].takeoffType and _assets[1].takeoffType==COORDINATE.WaypointType.TurningPoint or false -- Check available parking for air asset units. if _assetcategory==Group.Category.AIRPLANE or _assetcategory==Group.Category.HELICOPTER then if self.airbase and self.airbase:GetCoalition()==self:GetCoalition() then - if self:IsRunwayOperational() then + if self:IsRunwayOperational() or _assetairstart then - local Parking=self:_FindParkingForAssets(self.airbase,_assets) - - --if Parking==nil and not (self.category==Airbase.Category.HELIPAD) then - if Parking==nil then - local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) - self:_InfoMessage(text, 5) - return false + if _assetairstart then + -- Airstart no need to check parking + else + + -- Check parking. + local Parking=self:_FindParkingForAssets(self.airbase,_assets) + + -- No parking? + if Parking==nil then + local text=string.format("Warehouse %s: Request denied! Not enough free parking spots for all requested assets at the moment.", self.alias) + self:_InfoMessage(text, 5) + return false + end end else @@ -7969,93 +7987,123 @@ function WAREHOUSE:_FindParkingForAssets(airbase, assets) -- Loop over all assets that need a parking psot. for _,asset in pairs(assets) do local _asset=asset --#WAREHOUSE.Assetitem - - -- Get terminal type of this asset - local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) - - -- Asset specific parking. - parking[_asset.uid]={} - - -- Loop over all units - each one needs a spot. - for i=1,_asset.nunits do - -- Asset name - local assetname=_asset.spawngroupname.."-"..tostring(i) - - -- Loop over all parking spots. - local gotit=false - for _,_parkingspot in pairs(parkingdata) do - local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot - - -- Check correct terminal type for asset. We don't want helos in shelters etc. - if AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) and self:_CheckParkingValid(parkingspot) and self:_CheckParkingAsset(parkingspot, asset) and airbase:_CheckParkingLists(parkingspot.TerminalID) then - - -- Coordinate of the parking spot. - local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE - local _termid=parkingspot.TerminalID - local free=true - local problem=nil - - -- Loop over all obstacles. - for _,obstacle in pairs(obstacles) do - - -- Check if aircraft overlaps with any obstacle. - local dist=_spot:Get2DDistance(obstacle.coord) - local safe=_overlap(_asset.size, obstacle.size, dist) - - -- Spot is blocked. - if not safe then - self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist)) - free=false - problem=obstacle - problem.dist=dist - break - else - --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist)) - end - - end - - -- Check if spot is free - if free then - - -- Add parkingspot for this asset unit. - table.insert(parking[_asset.uid], parkingspot) - - -- Debug - self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid)) - - -- Add the unit as obstacle so that this spot will not be available for the next unit. - table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"}) - - gotit=true - break + if not _asset.spawned then + -- Get terminal type of this asset + local terminaltype=asset.terminalType or self:_GetTerminal(asset.attribute, self:GetAirbaseCategory()) + + -- Asset specific parking. + parking[_asset.uid]={} + + -- Loop over all units - each one needs a spot. + for i=1,_asset.nunits do + + -- Asset name + local assetname=_asset.spawngroupname.."-"..tostring(i) + + -- Loop over all parking spots. + local gotit=false + for _,_parkingspot in pairs(parkingdata) do + local parkingspot=_parkingspot --Wrapper.Airbase#AIRBASE.ParkingSpot + + -- Parking valid? + local valid=true + + if asset.parkingIDs then + -- If asset has assigned parking spots, we take these no matter what. + valid=self:_CheckParkingAsset(parkingspot, asset) else - - -- Debug output for occupied spots. - if self.Debug then - local coord=problem.coord --Core.Point#COORDINATE - local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist) - self:I(self.lid..text) - coord:MarkToAll(string.format(text)) - else - self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) - end - + + -- Valid terminal type depending on attribute. + local validTerminal=AIRBASE._CheckTerminalType(parkingspot.TerminalType, terminaltype) + + -- Valid parking list. + local validParking=self:_CheckParkingValid(parkingspot) + + -- Black and white list. + local validBWlist=airbase:_CheckParkingLists(parkingspot.TerminalID) + + -- Debug info. + --env.info(string.format("FF validTerminal = %s", tostring(validTerminal))) + --env.info(string.format("FF validParking = %s", tostring(validParking))) + --env.info(string.format("FF validBWlist = %s", tostring(validBWlist))) + + -- Check if all are true + valid=validTerminal and validParking and validBWlist end - - else - self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType)) - end -- check terminal type - end -- loop over parking spots - - -- No parking spot for at least one asset :( - if not gotit then - self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid)) - return nil - end - end -- loop over asset units + + + -- Check correct terminal type for asset. We don't want helos in shelters etc. + if valid then + + -- Coordinate of the parking spot. + local _spot=parkingspot.Coordinate -- Core.Point#COORDINATE + local _termid=parkingspot.TerminalID + local free=true + local problem=nil + + -- Loop over all obstacles. + for _,obstacle in pairs(obstacles) do + + -- Check if aircraft overlaps with any obstacle. + local dist=_spot:Get2DDistance(obstacle.coord) + local safe=_overlap(_asset.size, obstacle.size, dist) + + -- Spot is blocked. + if not safe then + self:T3(self.lid..string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is NOT SAFE", assetname, _asset.uid, _termid, dist)) + free=false + problem=obstacle + problem.dist=dist + break + else + --env.info(string.format("FF asset=%s (id=%d): spot id=%d dist=%.1fm is SAFE", assetname, _asset.uid, _termid, dist)) + end + + end + + -- Check if spot is free + if free then + + -- Add parkingspot for this asset unit. + table.insert(parking[_asset.uid], parkingspot) + + -- Debug + self:T(self.lid..string.format("Parking spot %d is free for asset %s [id=%d]!", _termid, assetname, _asset.uid)) + + -- Add the unit as obstacle so that this spot will not be available for the next unit. + table.insert(obstacles, {coord=_spot, size=_asset.size, name=assetname, type="asset"}) + + gotit=true + break + + else + + -- Debug output for occupied spots. + if self.Debug then + local coord=problem.coord --Core.Point#COORDINATE + local text=string.format("Obstacle %s [type=%s] blocking spot=%d! Size=%.1f m and distance=%.1f m.", problem.name, problem.type, _termid, problem.size, problem.dist) + self:I(self.lid..text) + coord:MarkToAll(string.format(text)) + else + self:T(self.lid..string.format("Parking spot %d is occupied or not big enough!", _termid)) + end + + end + + else + self:T2(self.lid..string.format("Terminal ID=%d: type=%s not supported", parkingspot.TerminalID, parkingspot.TerminalType)) + end -- check terminal type + end -- loop over parking spots + + -- No parking spot for at least one asset :( + if not gotit then + self:I(self.lid..string.format("WARNING: No free parking spot for asset %s [id=%d]", assetname, _asset.uid)) + return nil + end + end -- loop over asset units + end -- Asset spawned check end -- loop over asset groups return parking diff --git a/Moose Development/Moose/Wrapper/Controllable.lua b/Moose Development/Moose/Wrapper/Controllable.lua index 5cf5b4ac2..44e95efe2 100644 --- a/Moose Development/Moose/Wrapper/Controllable.lua +++ b/Moose Development/Moose/Wrapper/Controllable.lua @@ -1416,7 +1416,7 @@ end -- @param #CONTROLLABLE self -- @param Core.Zone#ZONE Zone The zone where to land. -- @param #number Duration The duration in seconds to stay on the ground. --- @return #CONTROLLABLE self +-- @return DCS#Task The DCS task structure. function CONTROLLABLE:TaskLandAtZone( Zone, Duration, RandomPoint ) -- Get landing point From 793c0d988eb91fb3125de814f208411fe8ae895b Mon Sep 17 00:00:00 2001 From: Applevangelist Date: Tue, 3 Jan 2023 10:22:10 +0100 Subject: [PATCH 2/2] Changes from dev --- Moose Development/Moose/Core/Set.lua | 644 ++++++++++++++++++-- Moose Development/Moose/Utilities/Utils.lua | 76 ++- 2 files changed, 663 insertions(+), 57 deletions(-) diff --git a/Moose Development/Moose/Core/Set.lua b/Moose Development/Moose/Core/Set.lua index 82a7d65a5..b973172c1 100644 --- a/Moose Development/Moose/Core/Set.lua +++ b/Moose Development/Moose/Core/Set.lua @@ -53,6 +53,7 @@ do -- SET_BASE -- @field #table Index Table of indices. -- @field #table List Unused table. -- @field Core.Scheduler#SCHEDULER CallScheduler + -- @field #SET_BASE.Filters Filter Filters -- @extends Core.Base#BASE --- The @{Core.Set#SET_BASE} class defines the core functions that define a collection of objects. @@ -83,6 +84,11 @@ do -- SET_BASE YieldInterval = nil, } + --- Filters + -- @type SET_BASE.Filters + -- @field #table Coalition Coalitions + -- @field #table Prefix Prefixes. + --- Creates a new SET_BASE object, building a set of units belonging to a coalitions, categories, countries, types or with defined prefix names. -- @param #SET_BASE self -- @return #SET_BASE @@ -135,11 +141,12 @@ do -- SET_BASE --- Clear the Objects in the Set. -- @param #SET_BASE self + -- @param #boolean TriggerEvent If `true`, an event remove is triggered for each group that is removed from the set. -- @return #SET_BASE self - function SET_BASE:Clear() + function SET_BASE:Clear(TriggerEvent) for Name, Object in pairs( self.Set ) do - self:Remove( Name ) + self:Remove( Name, not TriggerEvent ) end return self @@ -166,7 +173,7 @@ do -- SET_BASE --- Gets a list of the Names of the Objects in the Set. -- @param #SET_BASE self - -- @return #SET_BASE self + -- @return #table Table of names. function SET_BASE:GetSetNames() -- R2.3 self:F2() @@ -181,7 +188,7 @@ do -- SET_BASE --- Returns a table of the Objects in the Set. -- @param #SET_BASE self - -- @return #SET_BASE self + -- @return #table Table of objects. function SET_BASE:GetSetObjects() -- R2.3 self:F2() @@ -197,16 +204,21 @@ do -- SET_BASE --- Removes a @{Core.Base#BASE} object from the @{Core.Set#SET_BASE} and derived classes, based on the Object Name. -- @param #SET_BASE self -- @param #string ObjectName - -- @param NoTriggerEvent (Optional) When `true`, the :Remove() method will not trigger a **Removed** event. + -- @param #boolean NoTriggerEvent (Optional) When `true`, the :Remove() method will not trigger a **Removed** event. function SET_BASE:Remove( ObjectName, NoTriggerEvent ) self:F2( { ObjectName = ObjectName } ) local TriggerEvent = true - if NoTriggerEvent then TriggerEvent = false end + if NoTriggerEvent then + TriggerEvent = false + else + TriggerEvent = true + end local Object = self.Set[ObjectName] if Object then + for Index, Key in ipairs( self.Index ) do if Key == ObjectName then table.remove( self.Index, Index ) @@ -214,6 +226,7 @@ do -- SET_BASE break end end + -- When NoTriggerEvent is true, then no Removed event will be triggered. if TriggerEvent then self:Removed( ObjectName, Object ) @@ -311,7 +324,6 @@ do -- SET_BASE -- @param #SET_BASE self -- @param Core.Set#SET_BASE SetB Set other set, called *B*. -- @return Core.Set#SET_BASE A set of objects that is included in set *A* **and** in set *B*. - function SET_BASE:GetSetIntersection(SetB) local intersection=SET_BASE:New() @@ -461,16 +473,32 @@ do -- SET_BASE -- @param #SET_BASE self -- @return #SET_BASE self function SET_BASE:FilterOnce() + + --self:Clear() for ObjectName, Object in pairs( self.Database ) do if self:IsIncludeObject( Object ) then self:Add( ObjectName, Object ) + else + self:Remove(ObjectName, true) end end return self end + + --- Clear all filters. You still need to apply :FilterOnce() + -- @param #SET_BASE self + -- @return #SET_BASE self + function SET_BASE:FilterClear() + + for key,value in pairs(self.Filter) do + self.Filter[key]={} + end + + return self + end --- Starts the filtering for the defined collection. -- @param #SET_BASE self @@ -817,7 +845,7 @@ do -- SET_BASE --- Decides whether an object is in the SET -- @param #SET_BASE self -- @param #table Object - -- @return #SET_BASE self + -- @return #boolean `true` if object is in set and `false` otherwise. function SET_BASE:IsInSet( Object ) self:F3( Object ) local outcome = false @@ -1021,9 +1049,9 @@ do -- SET_GROUP return self end - --- Gets the Set. + --- Get a *new* set that only contains alive groups. -- @param #SET_GROUP self - -- @return #table Table of objects + -- @return #SET_GROUP Set of alive groups. function SET_GROUP:GetAliveSet() self:F2() @@ -1169,11 +1197,14 @@ do -- SET_GROUP --- Builds a set of groups in zones. -- @param #SET_GROUP self -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE + -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self - function SET_GROUP:FilterZones( Zones ) - if not self.Filter.Zones then + function SET_GROUP:FilterZones( Zones, Clear ) + + if Clear or not self.Filter.Zones then self.Filter.Zones = {} end + local zones = {} if Zones.ClassName and Zones.ClassName == "SET_ZONE" then zones = Zones.Set @@ -1183,34 +1214,12 @@ do -- SET_GROUP else zones = Zones end + for _, Zone in pairs( zones ) do local zonename = Zone:GetName() self.Filter.Zones[zonename] = Zone end - return self - end - - --- Builds a set of groups in zones. - -- @param #SET_GROUP self - -- @param #table Zones Table of Core.Zone#ZONE Zone objects, or a Core.Set#SET_ZONE - -- @return #SET_GROUP self - function SET_GROUP:FilterZones( Zones ) - if not self.Filter.Zones then - self.Filter.Zones = {} - end - local zones = {} - if Zones.ClassName and Zones.ClassName == "SET_ZONE" then - zones = Zones.Set - elseif type( Zones ) ~= "table" or (type( Zones ) == "table" and Zones.ClassName ) then - self:E("***** FilterZones needs either a table of ZONE Objects or a SET_ZONE as parameter!") - return self - else - zones = Zones - end - for _,Zone in pairs( zones ) do - local zonename = Zone:GetName() - self.Filter.Zones[zonename] = Zone - end + return self end @@ -1218,17 +1227,21 @@ do -- SET_GROUP -- Possible current coalitions are red, blue and neutral. -- @param #SET_GROUP self -- @param #string Coalitions Can take the following values: "red", "blue", "neutral". + -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self - function SET_GROUP:FilterCoalitions( Coalitions ) - if not self.Filter.Coalitions then + function SET_GROUP:FilterCoalitions( Coalitions, Clear ) + + if Clear or (not self.Filter.Coalitions) then self.Filter.Coalitions = {} end - if type( Coalitions ) ~= "table" then - Coalitions = { Coalitions } - end + + -- Ensure table. + Coalitions = UTILS.EnsureTable(Coalitions, false) + for CoalitionID, Coalition in pairs( Coalitions ) do self.Filter.Coalitions[Coalition] = Coalition end + return self end @@ -1236,17 +1249,22 @@ do -- SET_GROUP -- Possible current categories are plane, helicopter, ground, ship. -- @param #SET_GROUP self -- @param #string Categories Can take the following values: "plane", "helicopter", "ground", "ship". + -- @param #boolean Clear If `true`, clear any previously defined filters. -- @return #SET_GROUP self - function SET_GROUP:FilterCategories( Categories ) - if not self.Filter.Categories then + function SET_GROUP:FilterCategories( Categories, Clear ) + + if Clear or not self.Filter.Categories then self.Filter.Categories = {} end + if type( Categories ) ~= "table" then Categories = { Categories } end + for CategoryID, Category in pairs( Categories ) do self.Filter.Categories[Category] = Category end + return self end @@ -1899,6 +1917,41 @@ do -- SET_GROUP end end + + --- Get the closest group of the set with respect to a given reference coordinate. Optionally, only groups of given coalitions are considered in the search. + -- @param #SET_GROUP self + -- @param Core.Point#COORDINATE Coordinate Reference Coordinate from which the closest group is determined. + -- @return Wrapper.Group#GROUP The closest group (if any). + -- @return #number Distance in meters to the closest group. + function SET_GROUP:GetClosestGroup(Coordinate, Coalitions) + + local Set = self:GetSet() + + local dmin=math.huge + local gmin=nil + + for GroupID, GroupData in pairs( Set ) do -- For each GROUP in SET_GROUP + local group=GroupData --Wrapper.Group#GROUP + + if group and group:IsAlive() and (Coalitions==nil or UTILS.IsAnyInTable(Coalitions, group:GetCoalition())) then + + local coord=group:GetCoord() + + -- Distance between ref. coordinate and group coordinate. + local d=UTILS.VecDist3D(Coordinate, coord) + + if d