From d25dea0ac1f7241f5eb6e273165dfb63a85b9d15 Mon Sep 17 00:00:00 2001
From: fajiao <1519100073@qq.com>
Date: Mon, 17 Oct 2022 18:57:07 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=86=85=E9=83=A8?=
=?UTF-8?q?=E8=B0=83=E7=94=A8=E8=AE=A1=E7=AE=97=E6=B5=81=E7=A8=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Cis.Application/AppConfig.json | 12 +-
Cis.Application/Core/Algo/HikMarkSeacher.cs | 8 +-
Cis.Application/Core/Algo/MarkSearcherBase.cs | 95 +++++++-----
Cis.Application/Core/Api/PtzServerApi.cs | 9 +-
.../Core/Center/CameraDataCenter.cs | 139 +++++++++++++-----
Cis.Application/Core/Common/Options.cs | 13 ++
Cis.Application/Core/Entity/CameraCalcInfo.cs | 66 +++++++++
Cis.Application/Startup.cs | 1 +
Cis.Application/Tb/Common/TbInfo.cs | 11 +-
.../Tb/Service/TbPtzCameraService.cs | 2 +-
Cis.Core/CoreConfig.json | 7 +-
Cis.Core/Extension/ObjectExtension.cs | 10 ++
12 files changed, 275 insertions(+), 98 deletions(-)
diff --git a/Cis.Application/AppConfig.json b/Cis.Application/AppConfig.json
index 07b7167..5c17e0c 100644
--- a/Cis.Application/AppConfig.json
+++ b/Cis.Application/AppConfig.json
@@ -1,4 +1,12 @@
{
- "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
-
+ "$schema": "https://gitee.com/dotnetchina/Furion/raw/v4/schemas/v4/furion-schema.json",
+ "CameraDataOptions": {
+ "LazyInit": false,
+ "LoopInterval": "1000"
+ },
+ "PTZServer": {
+ "Type": "",
+ "Ip": "127.0.0.1",
+ "Port": "7022"
+ }
}
\ No newline at end of file
diff --git a/Cis.Application/Core/Algo/HikMarkSeacher.cs b/Cis.Application/Core/Algo/HikMarkSeacher.cs
index 78cc79e..83e9bb9 100644
--- a/Cis.Application/Core/Algo/HikMarkSeacher.cs
+++ b/Cis.Application/Core/Algo/HikMarkSeacher.cs
@@ -40,8 +40,8 @@ public class HikMarkSeacher : MarkSearcherBase
protected override void CalcSensor()
{
- CameraCalcInfo.MinFocusX = 1783.6 / CameraCalcInfo.ImageWidth;
- CameraCalcInfo.MinFocusY = 1781.4 / CameraCalcInfo.ImageHeight;
+ _cameraCalcInfo.MinFocusX = 1783.6 / _cameraCalcInfo.ImageWidth;
+ _cameraCalcInfo.MinFocusY = 1781.4 / _cameraCalcInfo.ImageHeight;
}
#endregion Implement
@@ -50,13 +50,13 @@ public class HikMarkSeacher : MarkSearcherBase
protected virtual double GetFx(double zoomPos)
{
- CameraCalcInfo calcInfo = CameraCalcInfo;
+ CameraCalcInfo calcInfo = _cameraCalcInfo;
return calcInfo.MinFocusX * GetZoomTag(zoomPos);
}
protected virtual double GetFy(double zoomPos)
{
- CameraCalcInfo calcInfo = CameraCalcInfo;
+ CameraCalcInfo calcInfo = _cameraCalcInfo;
return calcInfo.MinFocusY * GetZoomTag(zoomPos);
}
diff --git a/Cis.Application/Core/Algo/MarkSearcherBase.cs b/Cis.Application/Core/Algo/MarkSearcherBase.cs
index ab8f86e..7e44e07 100644
--- a/Cis.Application/Core/Algo/MarkSearcherBase.cs
+++ b/Cis.Application/Core/Algo/MarkSearcherBase.cs
@@ -10,9 +10,9 @@ public abstract class MarkSearcherBase
///
/// 当前相机参数信息
///
- protected CameraCalcInfo CameraCalcInfo { get; set; }
+ protected CameraCalcInfo _cameraCalcInfo { get; set; }
- protected List MarkLabelInfoList { get; set; } = new();
+ protected List _markLabelInfoList { get; set; } = new();
protected MatrixBuilder MBuilder { get; set; } = Matrix.Build;
@@ -25,14 +25,7 @@ public abstract class MarkSearcherBase
public MarkSearcherBase(CameraCalcInfo cameraCalcInfo)
{
- InitCameraCalcInfoRelated(cameraCalcInfo);
- }
-
- protected void InitCameraCalcInfoRelated(CameraCalcInfo cameraCalcInfo)
- {
- CameraCalcInfo = cameraCalcInfo;
- CalcSensor();
- World2CameraMatrix = ConvertWorldToCamera(cameraCalcInfo);
+ UpdateCameraCalcInfoRelated(cameraCalcInfo);
}
#region Calc
@@ -41,23 +34,20 @@ public abstract class MarkSearcherBase
/// 计算标签位置过程
///
///
- public void Calc(CameraCalcInfo cameraCalcInfo)
+ public List Calc()
{
- //判断
- if (IsCameraRotate(cameraCalcInfo))
- InitCameraCalcInfoRelated(cameraCalcInfo);
+ List resultList = new();
- if (World2CameraMatrix == null || MarkLabelInfoList.Count == 0)
- return;
+ if (World2CameraMatrix == null || _markLabelInfoList.Count == 0)
+ return resultList;
- for (int index = 0; index < MarkLabelInfoList.Count; index++)
+ foreach (MarkLabelCalcInfo item in _markLabelInfoList)
{
- MarkLabelCalcInfo label = MarkLabelInfoList[index];
- Matrix labelC2WMatrix = ConvertCameraToWorld(label);
+ Matrix labelC2WMatrix = ConvertCameraToWorld(item);
Matrix labelPointMatrix = MBuilder.DenseOfArray(new double[,]
{
- { label.CanvasWidth / CameraCalcInfo.ImageWidth - 0.5 },
- { label.CanvasHeight / CameraCalcInfo.ImageHeight - 0.5 },
+ { item.CanvasWidth / _cameraCalcInfo.ImageWidth - 0.5 },
+ { item.CanvasHeight / _cameraCalcInfo.ImageHeight - 0.5 },
{ 1 }
});
Matrix lResult = labelC2WMatrix.Multiply(labelPointMatrix);
@@ -65,18 +55,19 @@ public abstract class MarkSearcherBase
double x = pResult[0, 0] / pResult[2, 0] + 0.5;
double y = pResult[1, 0] / pResult[2, 0] + 0.5;
- MarkLabelCalcResult labelCalcResult = new();
+ MarkLabelCalcResult labelCalcResult;
if (x > 0.99 || x < 0.01 || y > 0.99 || y < 0.01 || pResult[2, 0] < 0)
- {
- labelCalcResult.InFlag = false;
- }
+ labelCalcResult = MarkLabelCalcResult.New(item.Id, false);
else
- {
- labelCalcResult.InFlag = true;
- labelCalcResult.CanvasLeft = x * CameraCalcInfo.ImageWidth;
- labelCalcResult.CanvasTop = y * CameraCalcInfo.ImageHeight;
- }
+ labelCalcResult = MarkLabelCalcResult.New(item.Id, true, (x * _cameraCalcInfo.ImageWidth), (y * _cameraCalcInfo.ImageHeight));
+ resultList.Add(labelCalcResult);
}
+ return resultList;
+ }
+
+ public async Task> CalcAsync()
+ {
+ return await Task.Run(Calc);
}
///
@@ -84,12 +75,11 @@ public abstract class MarkSearcherBase
///
///
///
- protected bool IsCameraRotate(CameraCalcInfo newInfo)
+ protected bool IsCameraRotate(PtzInfo newInfo)
{
- return CameraCalcInfo == null ||
- CameraCalcInfo.PtzInfo.Pan != newInfo.PtzInfo.Pan ||
- CameraCalcInfo.PtzInfo.Tilt != newInfo.PtzInfo.Tilt ||
- CameraCalcInfo.PtzInfo.Zoom != newInfo.PtzInfo.Zoom;
+ return _cameraCalcInfo.PtzInfo.Pan != newInfo.Pan ||
+ _cameraCalcInfo.PtzInfo.Tilt != newInfo.Tilt ||
+ _cameraCalcInfo.PtzInfo.Zoom != newInfo.Zoom;
}
///
@@ -204,4 +194,39 @@ public abstract class MarkSearcherBase
protected abstract PointF GetFOfMatrixByZoomPos(double zoomPos);
#endregion Util
+
+ #region Operate Attr
+
+ protected void UpdateCameraCalcInfoRelated(CameraCalcInfo cameraCalcInfo)
+ {
+ _cameraCalcInfo = cameraCalcInfo;
+ CalcSensor();
+ World2CameraMatrix = ConvertWorldToCamera(cameraCalcInfo);
+ }
+
+ public void UpdateCameraCalcInfo(PtzInfo ptzInfo)
+ {
+ if (IsCameraRotate(ptzInfo))
+ {
+ _cameraCalcInfo.PtzInfo = ptzInfo;
+ World2CameraMatrix = ConvertWorldToCamera(_cameraCalcInfo);
+ }
+ }
+
+ public void AddMarkLabelCalcInfo(MarkLabelCalcInfo markLabelCalcInfo)
+ {
+ _markLabelInfoList.Add(markLabelCalcInfo);
+ }
+
+ public bool ExistsMarkLabelCalcInfo(object id)
+ {
+ foreach (MarkLabelCalcInfo info in _markLabelInfoList)
+ {
+ if (info.Id.Equals(id))
+ return true;
+ }
+ return false;
+ }
+
+ #endregion Operate Attr
}
\ No newline at end of file
diff --git a/Cis.Application/Core/Api/PtzServerApi.cs b/Cis.Application/Core/Api/PtzServerApi.cs
index 4f65872..f85b487 100644
--- a/Cis.Application/Core/Api/PtzServerApi.cs
+++ b/Cis.Application/Core/Api/PtzServerApi.cs
@@ -24,7 +24,7 @@ public class PtzServerApi : IPtzApi, ISingleton
_stream = _tcpClient.GetStream();
}
- public RequestRealControl GetPtzInfo(int cameraId)
+ public RequestRealControl GetPtzRrc(int cameraId)
{
RequestRealControl realControl = new();
try
@@ -54,6 +54,13 @@ public class PtzServerApi : IPtzApi, ISingleton
return realControl;
}
+ public PtzInfo GetPtzInfo(int cameraId)
+ {
+ RequestRealControl rrc = GetPtzRrc(cameraId);
+ PtzInfo ptzInfo = PtzInfo.New(rrc.PTZPositionInfo.FP, rrc.PTZPositionInfo.FT, rrc.PTZPositionInfo.FZ);
+ return ptzInfo;
+ }
+
public static byte[] StructToByte(object structObj)
{
//获取结构体大小
diff --git a/Cis.Application/Core/Center/CameraDataCenter.cs b/Cis.Application/Core/Center/CameraDataCenter.cs
index 3791f54..ea766c7 100644
--- a/Cis.Application/Core/Center/CameraDataCenter.cs
+++ b/Cis.Application/Core/Center/CameraDataCenter.cs
@@ -13,10 +13,29 @@ public class CameraDataCenter
private readonly SqlSugarRepository _cmMarkLableRep;
private readonly SqlSugarRepository _tbPtzCameraRep;
private readonly PtzServerApi _ptzServerApi;
+ private readonly CameraDataOptions options = App.GetOptions();
private Thread _thread { get; set; }
- private List _tbPtzCameraList { get; set; }
- private ConcurrentDictionary _cameraPtzInfoDict { get; set; }
+
+ ///
+ /// (cbCameraId, MarkSearcherBase)
+ ///
+ private ConcurrentDictionary _markSearcherDict { get; set; } = new();
+
+ ///
+ /// (cbCameraId, cbCameraIp)
+ ///
+ private Dictionary _cbCameraId2IpDict { get; set; } = new();
+
+ ///
+ /// (cameraIp, TbPtzCamera)
+ ///
+ private ConcurrentDictionary _tbPtzCameraDict { get; set; } = new();
+
+ ///
+ /// (cameraIp, PtzInfo)
+ ///
+ private ConcurrentDictionary _cameraPtzInfoDict { get; set; } = new();
#endregion Attr
@@ -31,29 +50,16 @@ public class CameraDataCenter
private void Init()
{
- // 初始化 tbPtzCameraList
- _tbPtzCameraList = _tbPtzCameraRep.GetList();
- // 根据 Ip 去重
- _tbPtzCameraList = _tbPtzCameraList.Where((a, i) => _tbPtzCameraList.FindIndex(b => b.Ip == a.Ip) == i).ToList();
-
- // 初始化 ptzInfoDict
- _cameraPtzInfoDict = new ConcurrentDictionary();
-
- // 初始化 thread
- _thread = new Thread(WorkLoop)
+ if (!options.LazyInit)
{
- IsBackground = true// 设置后台线程
- };
- _thread.Start();
- }
-
- private void LazyInit()
- {
- // 初始化 tbPtzCameraList
- _tbPtzCameraList = new();
-
- // 初始化 ptzInfoDict
- _cameraPtzInfoDict = new ConcurrentDictionary();
+ List list = _tbPtzCameraRep.GetList();
+ // 根据 Ip 去重
+ foreach (TbPtzCamera item in list)
+ {
+ if (!_tbPtzCameraDict.ContainsKey(item.Ip))
+ _tbPtzCameraDict[item.Ip] = item;
+ }
+ }
// 初始化 thread
_thread = new Thread(WorkLoop)
@@ -65,47 +71,100 @@ public class CameraDataCenter
#region Loop
+ ///
+ /// 循环运行
+ ///
private void WorkLoop()
{
while (true)
{
- GetPtzInfoByApi();
- Thread.Sleep(10000);
+ RefreshPtzInfoByApi();
+ RefreshMarkSearcher();
+ Thread.Sleep(options.LoopInterval);
}
}
- private void GetPtzInfoByApi()
+ private void RefreshPtzInfoByApi()
{
- foreach (TbPtzCamera item in _tbPtzCameraList)
+ foreach (TbPtzCamera item in _tbPtzCameraDict.Values)
{
- RequestRealControl rrc = _ptzServerApi.GetPtzInfo(item.CameraId);
- CameraCalcInfo ptzInfo = new()
- {
- Pan = rrc.PTZPositionInfo.FP,
- Tilt = rrc.PTZPositionInfo.FT,
- Zoom = rrc.PTZPositionInfo.FZ
- };
+ PtzInfo ptzInfo = _ptzServerApi.GetPtzInfo(item.CameraId);
_cameraPtzInfoDict[item.Ip] = ptzInfo;
}
}
- private void Calc()
+ private void RefreshMarkSearcher()
{
+ foreach (KeyValuePair pair in _markSearcherDict)
+ {
+ long cameraId = pair.Key;
+ MarkSearcherBase markSearcher = pair.Value;
+ string cameraIp = _cbCameraId2IpDict[cameraId];
+ if (cameraIp == null) continue;
+ PtzInfo ptzInfo = _cameraPtzInfoDict[cameraIp];
+ if (ptzInfo == null) continue;
+ markSearcher.UpdateCameraCalcInfo(ptzInfo);
+ List resultList = markSearcher.Calc();
+ }
}
#endregion Loop
#region external call
- public bool GetCamera(string ip)
+ ///
+ /// 激活 cbCamera 进入运算
+ ///
+ ///
+ ///
+ public bool ActiveCamera(long cameraId)
{
- return false;
+ CbCamera cbCamera = _cbCameraRep.GetById(cameraId);
+ if (cbCamera == null) return false;
+ TbPtzCamera tbPtzCamera;
+ string cameraIp = cbCamera.Ip;
+ if (!_tbPtzCameraDict.IsExistKey(cameraIp))
+ {
+ tbPtzCamera = _tbPtzCameraRep.GetFirst(u => u.Ip == cameraIp);
+ if (tbPtzCamera == null) return false;
+ _tbPtzCameraDict[cameraIp] = tbPtzCamera;
+ }
+ else
+ {
+ tbPtzCamera = _tbPtzCameraDict[cameraIp];
+ }
+ _cbCameraId2IpDict[cbCamera.Id] = cameraIp;
+ CameraCalcInfo cameraCalcInfo = CameraCalcInfo.New(cameraId, _ptzServerApi.GetPtzInfo(tbPtzCamera.Id));
+ HikMarkSeacher markSeacher = new(cameraCalcInfo);
+ List cmMarkLabelList = _cmMarkLableRep.GetList(u => u.CbCameraId == cameraId);
+ foreach (CmMarkLabel item in cmMarkLabelList)
+ {
+ MarkLabelCalcInfo markLabelCalcInfo = MarkLabelCalcInfo.New(
+ item.Id,
+ PtzInfo.New(item.PanPosition, item.TiltPosition, item.ZoomPosition),
+ item.CanvasWidth,
+ item.CanvasHeight,
+ item.CanvasLeft,
+ item.CanvasTop
+ );
+ markSeacher.AddMarkLabelCalcInfo(markLabelCalcInfo);
+ }
+ _markSearcherDict[cameraId] = markSeacher;
+ return true;
}
- public bool ActiveCamera(string ip)
+ ///
+ /// 解除 cbCamera 进入运算
+ ///
+ ///
+ ///
+ public bool DeActiveCamera(long cameraId)
{
+ return false;
+ }
-
+ public bool UpdateCamera(long cameraId, long markLabelId)
+ {
return false;
}
diff --git a/Cis.Application/Core/Common/Options.cs b/Cis.Application/Core/Common/Options.cs
index c121c2a..6eb32fa 100644
--- a/Cis.Application/Core/Common/Options.cs
+++ b/Cis.Application/Core/Common/Options.cs
@@ -1,5 +1,18 @@
namespace Cis.Application.Core;
+public class CameraDataOptions : IConfigurableOptions
+{
+ ///
+ /// 是否懒加载
+ ///
+ public bool LazyInit { get; set; }
+
+ ///
+ /// 循环间隔,单位毫秒
+ ///
+ public int LoopInterval { get; set; }
+}
+
///
/// PtzServer选项
///
diff --git a/Cis.Application/Core/Entity/CameraCalcInfo.cs b/Cis.Application/Core/Entity/CameraCalcInfo.cs
index 49ff7b7..f4c6bf7 100644
--- a/Cis.Application/Core/Entity/CameraCalcInfo.cs
+++ b/Cis.Application/Core/Entity/CameraCalcInfo.cs
@@ -5,6 +5,11 @@
///
public class CameraCalcInfo
{
+ ///
+ /// Camera Id
+ ///
+ public long Id { get; set; }
+
///
/// Ptz 信息
///
@@ -26,6 +31,15 @@ public class CameraCalcInfo
public double MinFocusX { get; set; } = 0f;
public double MinFocusY { get; set; } = 0f;
+
+ public static CameraCalcInfo New(long id, PtzInfo ptzInfo)
+ {
+ return new()
+ {
+ Id = id,
+ PtzInfo = ptzInfo
+ };
+ }
}
///
@@ -47,6 +61,16 @@ public class PtzInfo
/// Zoom 坐标
///
public double Zoom { get; set; }
+
+ public static PtzInfo New(double pan, double tilt, double zoom)
+ {
+ return new()
+ {
+ Pan = pan,
+ Tilt = tilt,
+ Zoom = zoom
+ };
+ }
}
///
@@ -54,6 +78,11 @@ public class PtzInfo
///
public class MarkLabelCalcInfo
{
+ ///
+ /// MarkLabel Id
+ ///
+ public long Id { get; set; }
+
///
/// Ptz 信息
///
@@ -78,6 +107,19 @@ public class MarkLabelCalcInfo
/// 画布 top 距离
///
public double CanvasTop { get; set; }
+
+ public static MarkLabelCalcInfo New(long id, PtzInfo ptzInfo, double canvasWidth, double canvasHeight, double canvasLeft, double canvasTop)
+ {
+ return new()
+ {
+ Id = id,
+ PtzInfo = ptzInfo,
+ CanvasWidth = canvasWidth,
+ CanvasHeight = canvasHeight,
+ CanvasLeft = canvasLeft,
+ CanvasTop = canvasTop
+ };
+ }
}
///
@@ -85,6 +127,11 @@ public class MarkLabelCalcInfo
///
public class MarkLabelCalcResult
{
+ ///
+ /// MarkLabel Id
+ ///
+ public long Id { get; set; }
+
///
/// true 显示(在当前视频画面里面)
/// false 不显示(不在当前视频画面里面)
@@ -101,4 +148,23 @@ public class MarkLabelCalcResult
///
public double CanvasTop { get; set; }
+ public static MarkLabelCalcResult New(long id, bool inFlag)
+ {
+ return new()
+ {
+ Id = id,
+ InFlag = inFlag
+ };
+ }
+
+ public static MarkLabelCalcResult New(long id, bool inFlag, double canvasLeft, double canvasTop)
+ {
+ return new()
+ {
+ Id = id,
+ InFlag = inFlag,
+ CanvasLeft = canvasLeft,
+ CanvasTop = canvasTop
+ };
+ }
}
\ No newline at end of file
diff --git a/Cis.Application/Startup.cs b/Cis.Application/Startup.cs
index 19a7348..43c51e4 100644
--- a/Cis.Application/Startup.cs
+++ b/Cis.Application/Startup.cs
@@ -14,6 +14,7 @@ public class Startup : AppStartup
///
public void ConfigureServices(IServiceCollection services)
{
+ services.AddConfigurableOptions();
services.AddConfigurableOptions();
services.AddSingleton(new CameraDataCenter());
diff --git a/Cis.Application/Tb/Common/TbInfo.cs b/Cis.Application/Tb/Common/TbInfo.cs
index 135f94d..b2e4830 100644
--- a/Cis.Application/Tb/Common/TbInfo.cs
+++ b/Cis.Application/Tb/Common/TbInfo.cs
@@ -1,10 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Cis.Application.Tb;
+namespace Cis.Application.Tb;
public class TbInfo
{
@@ -25,5 +19,4 @@ public class TbInfo
public const string TbPtzCameraTbName = "tb_ptzcamera";
#endregion Table Info
-}
-
+}
\ No newline at end of file
diff --git a/Cis.Application/Tb/Service/TbPtzCameraService.cs b/Cis.Application/Tb/Service/TbPtzCameraService.cs
index 03cb5bf..724403f 100644
--- a/Cis.Application/Tb/Service/TbPtzCameraService.cs
+++ b/Cis.Application/Tb/Service/TbPtzCameraService.cs
@@ -19,7 +19,7 @@ public class TbPtzCameraService : ITransient
public async Task> GetList(string queryJson = "")
{
- JObject queryObj = !string.IsNullOrEmpty(queryJson) ? queryJson.ToJObject():default;
+ JObject queryObj = !string.IsNullOrEmpty(queryJson) ? queryJson.ToJObject() : default;
List list = await _tbPtzCameraRep.AsQueryable()
.ToListAsync();
return list;
diff --git a/Cis.Core/CoreConfig.json b/Cis.Core/CoreConfig.json
index 471e3b9..ac54671 100644
--- a/Cis.Core/CoreConfig.json
+++ b/Cis.Core/CoreConfig.json
@@ -21,14 +21,9 @@
]
},
"Cache": {
- "CacheType": "Redis", // Memory、Redis
+ "CacheType": "Memory", // Memory、Redis
"RedisConnectionString": "127.0.0.1:6379;password=123456;db=2"
},
- "PTZServer": {
- "Type": "",
- "Ip": "127.0.0.1",
- "Port": "7022"
- },
"AppSettings": {
"InjectSpecificationDocument": true // 生产环境是否开启Swagger
},
diff --git a/Cis.Core/Extension/ObjectExtension.cs b/Cis.Core/Extension/ObjectExtension.cs
index a080cf4..635ce49 100644
--- a/Cis.Core/Extension/ObjectExtension.cs
+++ b/Cis.Core/Extension/ObjectExtension.cs
@@ -138,4 +138,14 @@ public static class ObjectExtension
var sc = pi.GetCustomAttributes(false).FirstOrDefault(u => u.IsIgnore == true);
return sc != null;
}
+
+ public static bool IsExistKey(this IDictionary dict, object key)
+ {
+ foreach (object item in dict.Keys)
+ {
+ if (Equals(key, item))
+ return true;
+ }
+ return false;
+ }
}
\ No newline at end of file