DynamicTextureModule.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. /*
  2. * Copyright (c) Contributors, http://opensimulator.org/
  3. * See CONTRIBUTORS.TXT for a full list of copyright holders.
  4. *
  5. * Redistribution and use in source and binary forms, with or without
  6. * modification, are permitted provided that the following conditions are met:
  7. * * Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * * Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. * * Neither the name of the OpenSimulator Project nor the
  13. * names of its contributors may be used to endorse or promote products
  14. * derived from this software without specific prior written permission.
  15. *
  16. * THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS ``AS IS'' AND ANY
  17. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  18. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  19. * DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
  20. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  21. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
  22. * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
  23. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  24. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
  25. * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  26. */
  27. using System;
  28. using System.Collections.Generic;
  29. using System.Drawing;
  30. using System.Drawing.Imaging;
  31. using Nini.Config;
  32. using OpenMetaverse;
  33. using OpenMetaverse.Imaging;
  34. using OpenSim.Framework;
  35. using OpenSim.Region.Framework.Interfaces;
  36. using OpenSim.Region.Framework.Scenes;
  37. using log4net;
  38. using System.Reflection;
  39. using Mono.Addins;
  40. namespace OpenSim.Region.CoreModules.Scripting.DynamicTexture
  41. {
  42. [Extension(Path = "/OpenSim/RegionModules", NodeName = "RegionModule", Id = "DynamicTextureModule")]
  43. public class DynamicTextureModule : ISharedRegionModule, IDynamicTextureManager
  44. {
  45. // private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
  46. private const int ALL_SIDES = -1;
  47. public const int DISP_EXPIRE = 1;
  48. public const int DISP_TEMP = 2;
  49. /// <summary>
  50. /// If true then where possible dynamic textures are reused.
  51. /// </summary>
  52. public bool ReuseTextures { get; set; }
  53. /// <summary>
  54. /// If false, then textures which have a low data size are not reused when ReuseTextures = true.
  55. /// </summary>
  56. /// <remarks>
  57. /// LL viewers 3.3.4 and before appear to not fully render textures pulled from the viewer cache if those
  58. /// textures have a relatively high pixel surface but a small data size. Typically, this appears to happen
  59. /// if the data size is smaller than the viewer's discard level 2 size estimate. So if this is setting is
  60. /// false, textures smaller than the calculation in IsSizeReuseable are always regenerated rather than reused
  61. /// to work around this problem.</remarks>
  62. public bool ReuseLowDataTextures { get; set; }
  63. private Dictionary<UUID, Scene> RegisteredScenes = new Dictionary<UUID, Scene>();
  64. private Dictionary<string, IDynamicTextureRender> RenderPlugins =
  65. new Dictionary<string, IDynamicTextureRender>();
  66. private Dictionary<UUID, DynamicTextureUpdater> Updaters = new Dictionary<UUID, DynamicTextureUpdater>();
  67. /// <summary>
  68. /// Record dynamic textures that we can reuse for a given data and parameter combination rather than
  69. /// regenerate.
  70. /// </summary>
  71. /// <remarks>
  72. /// Key is string.Format("{0}{1}", data
  73. /// </remarks>
  74. private Cache m_reuseableDynamicTextures;
  75. /// <summary>
  76. /// This constructor is only here because of the Unit Tests...
  77. /// Don't use it.
  78. /// </summary>
  79. public DynamicTextureModule()
  80. {
  81. m_reuseableDynamicTextures = new Cache(CacheMedium.Memory, CacheStrategy.Conservative);
  82. m_reuseableDynamicTextures.DefaultTTL = new TimeSpan(24, 0, 0);
  83. }
  84. #region IDynamicTextureManager Members
  85. public void RegisterRender(string handleType, IDynamicTextureRender render)
  86. {
  87. if (!RenderPlugins.ContainsKey(handleType))
  88. {
  89. RenderPlugins.Add(handleType, render);
  90. }
  91. }
  92. /// <summary>
  93. /// Called by code which actually renders the dynamic texture to supply texture data.
  94. /// </summary>
  95. /// <param name="updaterId"></param>
  96. /// <param name="texture"></param>
  97. public void ReturnData(UUID updaterId, IDynamicTexture texture)
  98. {
  99. DynamicTextureUpdater updater = null;
  100. lock (Updaters)
  101. {
  102. if (Updaters.ContainsKey(updaterId))
  103. {
  104. updater = Updaters[updaterId];
  105. }
  106. }
  107. if (updater != null)
  108. {
  109. if (RegisteredScenes.ContainsKey(updater.SimUUID))
  110. {
  111. Scene scene = RegisteredScenes[updater.SimUUID];
  112. UUID newTextureID = updater.DataReceived(texture.Data, scene);
  113. if (ReuseTextures
  114. && !updater.BlendWithOldTexture
  115. && texture.IsReuseable
  116. && (ReuseLowDataTextures || IsDataSizeReuseable(texture)))
  117. {
  118. m_reuseableDynamicTextures.Store(
  119. GenerateReusableTextureKey(texture.InputCommands, texture.InputParams), newTextureID);
  120. }
  121. }
  122. }
  123. if (updater.UpdateTimer == 0)
  124. {
  125. lock (Updaters)
  126. {
  127. if (!Updaters.ContainsKey(updater.UpdaterID))
  128. {
  129. Updaters.Remove(updater.UpdaterID);
  130. }
  131. }
  132. }
  133. }
  134. /// <summary>
  135. /// Determines whether the texture is reuseable based on its data size.
  136. /// </summary>
  137. /// <remarks>
  138. /// This is a workaround for a viewer bug where very small data size textures relative to their pixel size
  139. /// are not redisplayed properly when pulled from cache. The calculation here is based on the typical discard
  140. /// level of 2, a 'rate' of 0.125 and 4 components (which makes for a factor of 0.5).
  141. /// </remarks>
  142. /// <returns></returns>
  143. private bool IsDataSizeReuseable(IDynamicTexture texture)
  144. {
  145. // Console.WriteLine("{0} {1}", texture.Size.Width, texture.Size.Height);
  146. int discardLevel2DataThreshold = (int)Math.Ceiling((texture.Size.Width >> 2) * (texture.Size.Height >> 2) * 0.5);
  147. // m_log.DebugFormat(
  148. // "[DYNAMIC TEXTURE MODULE]: Discard level 2 threshold {0}, texture data length {1}",
  149. // discardLevel2DataThreshold, texture.Data.Length);
  150. return discardLevel2DataThreshold < texture.Data.Length;
  151. }
  152. public UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url,
  153. string extraParams, int updateTimer)
  154. {
  155. return AddDynamicTextureURL(simID, primID, contentType, url, extraParams, updateTimer, false, 255);
  156. }
  157. public UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url,
  158. string extraParams, int updateTimer, bool SetBlending, byte AlphaValue)
  159. {
  160. return AddDynamicTextureURL(simID, primID, contentType, url,
  161. extraParams, updateTimer, SetBlending,
  162. (int)(DISP_TEMP|DISP_EXPIRE), AlphaValue, ALL_SIDES);
  163. }
  164. public UUID AddDynamicTextureURL(UUID simID, UUID primID, string contentType, string url,
  165. string extraParams, int updateTimer, bool SetBlending,
  166. int disp, byte AlphaValue, int face)
  167. {
  168. if (RenderPlugins.ContainsKey(contentType))
  169. {
  170. DynamicTextureUpdater updater = new DynamicTextureUpdater();
  171. updater.SimUUID = simID;
  172. updater.PrimID = primID;
  173. updater.ContentType = contentType;
  174. updater.Url = url;
  175. updater.UpdateTimer = updateTimer;
  176. updater.UpdaterID = UUID.Random();
  177. updater.Params = extraParams;
  178. updater.BlendWithOldTexture = SetBlending;
  179. updater.FrontAlpha = AlphaValue;
  180. updater.Face = face;
  181. updater.Disp = disp;
  182. lock (Updaters)
  183. {
  184. if (!Updaters.ContainsKey(updater.UpdaterID))
  185. {
  186. Updaters.Add(updater.UpdaterID, updater);
  187. }
  188. }
  189. RenderPlugins[contentType].AsyncConvertUrl(updater.UpdaterID, url, extraParams);
  190. return updater.UpdaterID;
  191. }
  192. return UUID.Zero;
  193. }
  194. public UUID AddDynamicTextureData(UUID simID, UUID primID, string contentType, string data,
  195. string extraParams, int updateTimer)
  196. {
  197. return AddDynamicTextureData(simID, primID, contentType, data, extraParams, updateTimer, false, 255);
  198. }
  199. public UUID AddDynamicTextureData(UUID simID, UUID primID, string contentType, string data,
  200. string extraParams, int updateTimer, bool SetBlending, byte AlphaValue)
  201. {
  202. return AddDynamicTextureData(simID, primID, contentType, data, extraParams, updateTimer, SetBlending,
  203. (int) (DISP_TEMP|DISP_EXPIRE), AlphaValue, ALL_SIDES);
  204. }
  205. public UUID AddDynamicTextureData(UUID simID, UUID primID, string contentType, string data,
  206. string extraParams, int updateTimer, bool SetBlending, int disp, byte AlphaValue, int face)
  207. {
  208. if (!RenderPlugins.ContainsKey(contentType))
  209. return UUID.Zero;
  210. Scene scene;
  211. RegisteredScenes.TryGetValue(simID, out scene);
  212. if (scene == null)
  213. return UUID.Zero;
  214. SceneObjectPart part = scene.GetSceneObjectPart(primID);
  215. if (part == null)
  216. return UUID.Zero;
  217. // If we want to reuse dynamic textures then we have to ignore any request from the caller to expire
  218. // them.
  219. if (ReuseTextures)
  220. disp = disp & ~DISP_EXPIRE;
  221. DynamicTextureUpdater updater = new DynamicTextureUpdater();
  222. updater.SimUUID = simID;
  223. updater.PrimID = primID;
  224. updater.ContentType = contentType;
  225. updater.BodyData = data;
  226. updater.UpdateTimer = updateTimer;
  227. updater.UpdaterID = UUID.Random();
  228. updater.Params = extraParams;
  229. updater.BlendWithOldTexture = SetBlending;
  230. updater.FrontAlpha = AlphaValue;
  231. updater.Face = face;
  232. updater.Url = "Local image";
  233. updater.Disp = disp;
  234. object objReusableTextureUUID = null;
  235. if (ReuseTextures && !updater.BlendWithOldTexture)
  236. {
  237. string reuseableTextureKey = GenerateReusableTextureKey(data, extraParams);
  238. objReusableTextureUUID = m_reuseableDynamicTextures.Get(reuseableTextureKey);
  239. if (objReusableTextureUUID != null)
  240. {
  241. // If something else has removed this temporary asset from the cache, detect and invalidate
  242. // our cached uuid.
  243. if (scene.AssetService.GetMetadata(objReusableTextureUUID.ToString()) == null)
  244. {
  245. m_reuseableDynamicTextures.Invalidate(reuseableTextureKey);
  246. objReusableTextureUUID = null;
  247. }
  248. }
  249. }
  250. // We cannot reuse a dynamic texture if the data is going to be blended with something already there.
  251. if (objReusableTextureUUID == null)
  252. {
  253. lock (Updaters)
  254. {
  255. if (!Updaters.ContainsKey(updater.UpdaterID))
  256. {
  257. Updaters.Add(updater.UpdaterID, updater);
  258. }
  259. }
  260. // m_log.DebugFormat(
  261. // "[DYNAMIC TEXTURE MODULE]: Requesting generation of new dynamic texture for {0} in {1}",
  262. // part.Name, part.ParentGroup.Scene.Name);
  263. RenderPlugins[contentType].AsyncConvertData(updater.UpdaterID, data, extraParams);
  264. }
  265. else
  266. {
  267. // m_log.DebugFormat(
  268. // "[DYNAMIC TEXTURE MODULE]: Reusing cached texture {0} for {1} in {2}",
  269. // objReusableTextureUUID, part.Name, part.ParentGroup.Scene.Name);
  270. // No need to add to updaters as the texture is always the same. Not that this functionality
  271. // apppears to be implemented anyway.
  272. updater.UpdatePart(part, (UUID)objReusableTextureUUID);
  273. }
  274. return updater.UpdaterID;
  275. }
  276. private string GenerateReusableTextureKey(string data, string extraParams)
  277. {
  278. return string.Format("{0}{1}", data, extraParams);
  279. }
  280. public void GetDrawStringSize(string contentType, string text, string fontName, int fontSize,
  281. out double xSize, out double ySize)
  282. {
  283. xSize = 0;
  284. ySize = 0;
  285. if (RenderPlugins.ContainsKey(contentType))
  286. {
  287. RenderPlugins[contentType].GetDrawStringSize(text, fontName, fontSize, out xSize, out ySize);
  288. }
  289. }
  290. #endregion
  291. #region ISharedRegionModule Members
  292. public void Initialise(IConfigSource config)
  293. {
  294. IConfig texturesConfig = config.Configs["Textures"];
  295. if (texturesConfig != null)
  296. {
  297. ReuseTextures = texturesConfig.GetBoolean("ReuseDynamicTextures", false);
  298. ReuseLowDataTextures = texturesConfig.GetBoolean("ReuseDynamicLowDataTextures", false);
  299. if (ReuseTextures)
  300. {
  301. m_reuseableDynamicTextures = new Cache(CacheMedium.Memory, CacheStrategy.Conservative);
  302. m_reuseableDynamicTextures.DefaultTTL = new TimeSpan(24, 0, 0);
  303. }
  304. }
  305. }
  306. public void PostInitialise()
  307. {
  308. }
  309. public void AddRegion(Scene scene)
  310. {
  311. if (!RegisteredScenes.ContainsKey(scene.RegionInfo.RegionID))
  312. {
  313. RegisteredScenes.Add(scene.RegionInfo.RegionID, scene);
  314. scene.RegisterModuleInterface<IDynamicTextureManager>(this);
  315. }
  316. }
  317. public void RegionLoaded(Scene scene)
  318. {
  319. }
  320. public void RemoveRegion(Scene scene)
  321. {
  322. if (RegisteredScenes.ContainsKey(scene.RegionInfo.RegionID))
  323. RegisteredScenes.Remove(scene.RegionInfo.RegionID);
  324. }
  325. public void Close()
  326. {
  327. }
  328. public string Name
  329. {
  330. get { return "DynamicTextureModule"; }
  331. }
  332. public Type ReplaceableInterface
  333. {
  334. get { return null; }
  335. }
  336. #endregion
  337. #region Nested type: DynamicTextureUpdater
  338. public class DynamicTextureUpdater
  339. {
  340. private static readonly ILog m_log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
  341. public bool BlendWithOldTexture = false;
  342. public string BodyData;
  343. public string ContentType;
  344. public byte FrontAlpha = 255;
  345. public string Params;
  346. public UUID PrimID;
  347. public bool SetNewFrontAlpha = false;
  348. public UUID SimUUID;
  349. public UUID UpdaterID;
  350. public int UpdateTimer;
  351. public int Face;
  352. public int Disp;
  353. public string Url;
  354. public DynamicTextureUpdater()
  355. {
  356. UpdateTimer = 0;
  357. BodyData = null;
  358. }
  359. /// <summary>
  360. /// Update the given part with the new texture.
  361. /// </summary>
  362. /// <returns>
  363. /// The old texture UUID.
  364. /// </returns>
  365. public UUID UpdatePart(SceneObjectPart part, UUID textureID)
  366. {
  367. UUID oldID;
  368. lock (part)
  369. {
  370. // mostly keep the values from before
  371. Primitive.TextureEntry tmptex = part.Shape.Textures;
  372. // FIXME: Need to return the appropriate ID if only a single face is replaced.
  373. oldID = tmptex.DefaultTexture.TextureID;
  374. if (Face == ALL_SIDES)
  375. {
  376. oldID = tmptex.DefaultTexture.TextureID;
  377. tmptex.DefaultTexture.TextureID = textureID;
  378. }
  379. else
  380. {
  381. try
  382. {
  383. Primitive.TextureEntryFace texface = tmptex.CreateFace((uint)Face);
  384. texface.TextureID = textureID;
  385. tmptex.FaceTextures[Face] = texface;
  386. }
  387. catch (Exception)
  388. {
  389. tmptex.DefaultTexture.TextureID = textureID;
  390. }
  391. }
  392. // I'm pretty sure we always want to force this to true
  393. // I'm pretty sure noone whats to set fullbright true if it wasn't true before.
  394. // tmptex.DefaultTexture.Fullbright = true;
  395. part.UpdateTextureEntry(tmptex.GetBytes());
  396. }
  397. return oldID;
  398. }
  399. /// <summary>
  400. /// Called once new texture data has been received for this updater.
  401. /// </summary>
  402. /// <param name="data"></param>
  403. /// <param name="scene"></param>
  404. /// <param name="isReuseable">True if the data given is reuseable.</param>
  405. /// <returns>The asset UUID given to the incoming data.</returns>
  406. public UUID DataReceived(byte[] data, Scene scene)
  407. {
  408. SceneObjectPart part = scene.GetSceneObjectPart(PrimID);
  409. if (part == null || data == null || data.Length <= 1)
  410. {
  411. string msg =
  412. String.Format("DynamicTextureModule: Error preparing image using URL {0}", Url);
  413. scene.SimChat(Utils.StringToBytes(msg), ChatTypeEnum.Say,
  414. 0, part.ParentGroup.RootPart.AbsolutePosition, part.Name, part.UUID, false);
  415. return UUID.Zero;
  416. }
  417. byte[] assetData = null;
  418. AssetBase oldAsset = null;
  419. if (BlendWithOldTexture)
  420. {
  421. Primitive.TextureEntryFace defaultFace = part.Shape.Textures.DefaultTexture;
  422. if (defaultFace != null)
  423. {
  424. oldAsset = scene.AssetService.Get(defaultFace.TextureID.ToString());
  425. if (oldAsset != null)
  426. assetData = BlendTextures(data, oldAsset.Data, SetNewFrontAlpha, FrontAlpha);
  427. }
  428. }
  429. if (assetData == null)
  430. {
  431. assetData = new byte[data.Length];
  432. Array.Copy(data, assetData, data.Length);
  433. }
  434. // Create a new asset for user
  435. AssetBase asset
  436. = new AssetBase(
  437. UUID.Random(), "DynamicImage" + Util.RandomClass.Next(1, 10000), (sbyte)AssetType.Texture,
  438. scene.RegionInfo.RegionID.ToString());
  439. asset.Data = assetData;
  440. asset.Description = String.Format("URL image : {0}", Url);
  441. if (asset.Description.Length > 128)
  442. asset.Description = asset.Description.Substring(0, 128);
  443. asset.Local = true; // dynamic images aren't saved in the assets server
  444. asset.Temporary = ((Disp & DISP_TEMP) != 0);
  445. scene.AssetService.Store(asset); // this will only save the asset in the local asset cache
  446. IJ2KDecoder cacheLayerDecode = scene.RequestModuleInterface<IJ2KDecoder>();
  447. if (cacheLayerDecode != null)
  448. {
  449. if (!cacheLayerDecode.Decode(asset.FullID, asset.Data))
  450. m_log.WarnFormat(
  451. "[DYNAMIC TEXTURE MODULE]: Decoding of dynamically generated asset {0} for {1} in {2} failed",
  452. asset.ID, part.Name, part.ParentGroup.Scene.Name);
  453. }
  454. UUID oldID = UpdatePart(part, asset.FullID);
  455. if (oldID != UUID.Zero && ((Disp & DISP_EXPIRE) != 0))
  456. {
  457. if (oldAsset == null)
  458. oldAsset = scene.AssetService.Get(oldID.ToString());
  459. if (oldAsset != null)
  460. {
  461. if (oldAsset.Temporary)
  462. {
  463. scene.AssetService.Delete(oldID.ToString());
  464. }
  465. }
  466. }
  467. return asset.FullID;
  468. }
  469. private byte[] BlendTextures(byte[] frontImage, byte[] backImage, bool setNewAlpha, byte newAlpha)
  470. {
  471. ManagedImage managedImage;
  472. Image image;
  473. if (OpenJPEG.DecodeToImage(frontImage, out managedImage, out image))
  474. {
  475. Bitmap image1 = new Bitmap(image);
  476. if (OpenJPEG.DecodeToImage(backImage, out managedImage, out image))
  477. {
  478. Bitmap image2 = new Bitmap(image);
  479. if (setNewAlpha)
  480. SetAlpha(ref image1, newAlpha);
  481. Bitmap joint = MergeBitMaps(image1, image2);
  482. byte[] result = new byte[0];
  483. try
  484. {
  485. result = OpenJPEG.EncodeFromImage(joint, true);
  486. }
  487. catch (Exception e)
  488. {
  489. m_log.ErrorFormat(
  490. "[DYNAMICTEXTUREMODULE]: OpenJpeg Encode Failed. Exception {0}{1}",
  491. e.Message, e.StackTrace);
  492. }
  493. return result;
  494. }
  495. }
  496. return null;
  497. }
  498. public Bitmap MergeBitMaps(Bitmap front, Bitmap back)
  499. {
  500. Bitmap joint;
  501. Graphics jG;
  502. joint = new Bitmap(back.Width, back.Height, PixelFormat.Format32bppArgb);
  503. jG = Graphics.FromImage(joint);
  504. jG.DrawImage(back, 0, 0, back.Width, back.Height);
  505. jG.DrawImage(front, 0, 0, back.Width, back.Height);
  506. return joint;
  507. }
  508. private void SetAlpha(ref Bitmap b, byte alpha)
  509. {
  510. for (int w = 0; w < b.Width; w++)
  511. {
  512. for (int h = 0; h < b.Height; h++)
  513. {
  514. b.SetPixel(w, h, Color.FromArgb(alpha, b.GetPixel(w, h)));
  515. }
  516. }
  517. }
  518. }
  519. #endregion
  520. }
  521. }