PageRenderTime 59ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/GeoRouting/branches/SharpMap/Data/Providers/SqlServer2008.cs

http://georouting-hyperpath.googlecode.com/
C# | 650 lines | 391 code | 65 blank | 194 comment | 49 complexity | 49be06a3a5d1d2ef3167979736ff8ac0 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
  1. // Copyright 2008 - William Dollins
  2. // SQL Server 2008 by William Dollins (dollins.bill@gmail.com)
  3. // Based on Oracle provider by Humberto Ferreira (humbertojdf@gmail.com)
  4. //
  5. // Date 2007-11-28
  6. //
  7. // This file is part of
  8. // is free software; you can redistribute it and/or modify
  9. // it under the terms of the GNU Lesser General Public License as published by
  10. // the Free Software Foundation; either version 2 of the License, or
  11. // (at your option) any later version.
  12. //
  13. // is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Lesser General Public License for more details.
  17. // You should have received a copy of the GNU Lesser General Public License
  18. // along with if not, write to the Free Software
  19. // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  20. using System;
  21. using System.Collections.ObjectModel;
  22. using System.Data.SqlClient;
  23. using SharpMap.Geometries;
  24. namespace SharpMap.Data.Providers
  25. {
  26. /// <summary>
  27. /// Possible spatial object types on SqlServer
  28. /// </summary>
  29. public enum SqlServerSpatialObjectType
  30. {
  31. /// <summary>
  32. /// Geometry
  33. /// </summary>
  34. Geometry,
  35. /// <summary>
  36. /// Geography
  37. /// </summary>
  38. Geography,
  39. }
  40. /// <summary>
  41. /// SQL Server 2008 data provider
  42. /// </summary>
  43. /// <remarks>
  44. /// <para>This provider was developed against the SQL Server 2008 November CTP. The platform may change significantly before release.</para>
  45. /// <example>
  46. /// Adding a datasource to a layer:
  47. /// <code lang="C#">
  48. /// Layers.VectorLayer myLayer = new Layers.VectorLayer("My layer");
  49. /// string ConnStr = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=myDB;Data Source=myServer\myInstance";
  50. /// myLayer.DataSource = new Data.Providers.Katmai(ConnStr, "myTable", "GeomColumn", "OidColumn");
  51. /// </code>
  52. /// </example>
  53. /// <para>SQL Server 2008 provider by Bill Dollins (dollins.bill@gmail.com). Based on the Oracle provider written by Humberto Ferreira.</para>
  54. /// </remarks>
  55. [Serializable]
  56. public class SqlServer2008 : IProvider
  57. {
  58. /// <summary>
  59. /// Initializes a new connection to SQL Server
  60. /// </summary>
  61. /// <param name="connectionStr">Connectionstring</param>
  62. /// <param name="tablename">Name of data table</param>
  63. /// <param name="geometryColumnName">Name of geometry column</param>
  64. /// <param name="oidColumnName">Name of column with unique identifier</param>
  65. public SqlServer2008(string connectionStr, string tablename, string geometryColumnName, string oidColumnName)
  66. :this(connectionStr, tablename, geometryColumnName, oidColumnName, SqlServerSpatialObjectType.Geometry)
  67. {
  68. }
  69. /// <summary>
  70. /// Initializes a new connection to SQL Server
  71. /// </summary>
  72. /// <param name="connectionStr">Connectionstring</param>
  73. /// <param name="tablename">Name of data table</param>
  74. /// <param name="geometryColumnName">Name of geometry column</param>
  75. /// <param name="oidColumnName">Name of column with unique identifier</param>
  76. /// <param name="spatialObjectType">The type of the spatial object to use for spatial queries</param>
  77. public SqlServer2008(string connectionStr, string tablename, string geometryColumnName, string oidColumnName, SqlServerSpatialObjectType spatialObjectType)
  78. {
  79. //Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=ztTest;Data Source=<server>\<instance>
  80. ConnectionString = connectionStr;
  81. Table = tablename;
  82. GeometryColumn = geometryColumnName;
  83. ObjectIdColumn = oidColumnName;
  84. _spatialObjectType = spatialObjectType;
  85. switch (spatialObjectType)
  86. {
  87. case SqlServerSpatialObjectType.Geometry:
  88. _spatialObject = "geometry";
  89. break;
  90. //case SqlServerSpatialObjectType.Geography:
  91. default:
  92. _spatialObject = "geography";
  93. break;
  94. }
  95. }
  96. /// <summary>
  97. /// Initializes a new connection to SQL Server
  98. /// </summary>
  99. /// <param name="connectionStr">Connectionstring</param>
  100. /// <param name="tablename">Name of data table</param>
  101. /// <param name="oidColumnName">Name of column with unique identifier</param>
  102. public SqlServer2008(string connectionStr, string tablename, string oidColumnName)
  103. : this(connectionStr, tablename, "shape", oidColumnName, SqlServerSpatialObjectType.Geometry)
  104. {
  105. }
  106. /// <summary>
  107. /// Initializes a new connection to SQL Server
  108. /// </summary>
  109. /// <param name="connectionStr">Connectionstring</param>
  110. /// <param name="tablename">Name of data table</param>
  111. /// <param name="oidColumnName">Name of column with unique identifier</param>
  112. /// <param name="spatialObjectType">The type of the spatial object to use for spatial queries</param>
  113. public SqlServer2008(string connectionStr, string tablename, string oidColumnName, SqlServerSpatialObjectType spatialObjectType)
  114. : this(connectionStr,tablename,"shape",oidColumnName, spatialObjectType)
  115. {
  116. }
  117. private bool _isOpen;
  118. /// <summary>
  119. /// Returns true if the datasource is currently open
  120. /// </summary>
  121. public bool IsOpen
  122. {
  123. get { return _isOpen; }
  124. }
  125. /// <summary>
  126. /// Opens the datasource
  127. /// </summary>
  128. public void Open()
  129. {
  130. //Don't really do anything.
  131. _isOpen = true;
  132. }
  133. /// <summary>
  134. /// Closes the datasource
  135. /// </summary>
  136. public void Close()
  137. {
  138. //Don't really do anything.
  139. _isOpen = false;
  140. }
  141. #region Disposers and finalizers
  142. private bool _disposed;
  143. /// <summary>
  144. /// Disposes the object
  145. /// </summary>
  146. public void Dispose()
  147. {
  148. Dispose(true);
  149. GC.SuppressFinalize(this);
  150. }
  151. internal void Dispose(bool disposing)
  152. {
  153. if (!_disposed)
  154. {
  155. if (disposing)
  156. {
  157. //Close();
  158. }
  159. _disposed = true;
  160. }
  161. }
  162. /// <summary>
  163. /// Finalizer
  164. /// </summary>
  165. ~SqlServer2008()
  166. {
  167. Dispose();
  168. }
  169. #endregion
  170. private string _connectionString;
  171. /// <summary>
  172. /// Connectionstring
  173. /// </summary>
  174. public string ConnectionString
  175. {
  176. get { return _connectionString; }
  177. set { _connectionString = value; }
  178. }
  179. private string _table;
  180. /// <summary>
  181. /// Data table name
  182. /// </summary>
  183. public string Table
  184. {
  185. get { return _table; }
  186. set { _table = value; }
  187. }
  188. private string _geometryColumn;
  189. /// <summary>
  190. /// Name of geometry column
  191. /// </summary>
  192. public string GeometryColumn
  193. {
  194. get { return _geometryColumn; }
  195. set { _geometryColumn = value; }
  196. }
  197. private string _objectIdColumn;
  198. /// <summary>
  199. /// Name of column that contains the Object ID
  200. /// </summary>
  201. public string ObjectIdColumn
  202. {
  203. get { return _objectIdColumn; }
  204. set { _objectIdColumn = value; }
  205. }
  206. private bool _makeValid;
  207. /// <summary>
  208. /// Gets/Sets whether all <see cref="SharpMap.Geometries"/> passed to SqlServer2008 should me made valid using this function.
  209. /// </summary>
  210. public Boolean ValidateGeometries { get { return _makeValid; } set { _makeValid = value; } }
  211. private String MakeValidString
  212. {
  213. get { return _makeValid ? ".MakeValid()" : String.Empty; }
  214. }
  215. private readonly SqlServerSpatialObjectType _spatialObjectType;
  216. private readonly string _spatialObject;
  217. /// <summary>
  218. /// Spatial object type for
  219. /// </summary>
  220. public SqlServerSpatialObjectType SpatialObjectType
  221. {
  222. get { return _spatialObjectType; }
  223. }
  224. /// <summary>
  225. /// Returns geometries within the specified bounding box
  226. /// </summary>
  227. /// <param name="bbox"></param>
  228. /// <returns></returns>
  229. public Collection<Geometry> GetGeometriesInView(BoundingBox bbox)
  230. {
  231. Collection<Geometry> features = new Collection<Geometry>();
  232. using (SqlConnection conn = new SqlConnection(_connectionString))
  233. {
  234. //Get bounding box string
  235. string strBbox = GetBoxFilterStr(bbox);
  236. string strSQL = "SELECT g." + GeometryColumn +".STAsBinary() ";
  237. strSQL += " FROM " + Table + " g WHERE ";
  238. if (!String.IsNullOrEmpty(_defintionQuery))
  239. strSQL += DefinitionQuery + " AND ";
  240. strSQL += strBbox;
  241. using (SqlCommand command = new SqlCommand(strSQL, conn))
  242. {
  243. conn.Open();
  244. using (SqlDataReader dr = command.ExecuteReader())
  245. {
  246. while (dr.Read())
  247. {
  248. if (dr[0] != DBNull.Value)
  249. {
  250. Geometry geom = Converters.WellKnownBinary.GeometryFromWKB.Parse((byte[])dr[0]);
  251. if(geom!=null)
  252. features.Add(geom);
  253. }
  254. }
  255. }
  256. conn.Close();
  257. }
  258. }
  259. return features;
  260. }
  261. /// <summary>
  262. /// Returns the geometry corresponding to the Object ID
  263. /// </summary>
  264. /// <param name="oid">Object ID</param>
  265. /// <returns>geometry</returns>
  266. public Geometry GetGeometryByID(uint oid)
  267. {
  268. Geometry geom = null;
  269. using (SqlConnection conn = new SqlConnection(_connectionString))
  270. {
  271. string strSQL = "SELECT g." + GeometryColumn + ".STAsBinary() FROM " + Table + " g WHERE " + ObjectIdColumn + "='" + oid + "'";
  272. conn.Open();
  273. using (SqlCommand command = new SqlCommand(strSQL, conn))
  274. {
  275. using (SqlDataReader dr = command.ExecuteReader())
  276. {
  277. while (dr.Read())
  278. {
  279. if (dr[0] != DBNull.Value)
  280. geom = Converters.WellKnownBinary.GeometryFromWKB.Parse((byte[])dr[0]);
  281. }
  282. }
  283. }
  284. conn.Close();
  285. }
  286. return geom;
  287. }
  288. /// <summary>
  289. /// Returns geometry Object IDs whose bounding box intersects 'bbox'
  290. /// </summary>
  291. /// <param name="bbox"></param>
  292. /// <returns></returns>
  293. public Collection<uint> GetObjectIDsInView(BoundingBox bbox)
  294. {
  295. Collection<uint> objectlist = new Collection<uint>();
  296. using (SqlConnection conn = new SqlConnection(_connectionString))
  297. {
  298. //Get bounding box string
  299. string strBbox = GetBoxFilterStr(bbox);
  300. string strSQL = "SELECT g." + ObjectIdColumn + " ";
  301. strSQL += "FROM " + Table + " g WHERE ";
  302. if (!String.IsNullOrEmpty(_defintionQuery))
  303. strSQL += DefinitionQuery + " AND ";
  304. strSQL += strBbox;
  305. using (SqlCommand command = new SqlCommand(strSQL, conn))
  306. {
  307. conn.Open();
  308. using (SqlDataReader dr = command.ExecuteReader())
  309. {
  310. while (dr.Read())
  311. {
  312. if (dr[0] != DBNull.Value)
  313. {
  314. uint id = (uint)(decimal)dr[0];
  315. objectlist.Add(id);
  316. }
  317. }
  318. }
  319. conn.Close();
  320. }
  321. }
  322. return objectlist;
  323. }
  324. /// <summary>
  325. /// Returns the box filter string needed in SQL query
  326. /// </summary>
  327. /// <param name="bbox"></param>
  328. /// <returns></returns>
  329. private string GetBoxFilterStr(BoundingBox bbox) {
  330. //geography::STGeomFromText('LINESTRING(47.656 -122.360, 47.656 -122.343)', 4326);
  331. LinearRing lr = new LinearRing();
  332. lr.Vertices.Add(new Point(bbox.Left, bbox.Bottom));
  333. lr.Vertices.Add(new Point(bbox.Right, bbox.Bottom));
  334. lr.Vertices.Add(new Point(bbox.Right, bbox.Top));
  335. lr.Vertices.Add(new Point(bbox.Left, bbox.Top));
  336. lr.Vertices.Add(new Point(bbox.Left, bbox.Bottom));
  337. Polygon p = new Polygon(lr);
  338. string bboxText = Converters.WellKnownText.GeometryToWKT.Write(p); // "";
  339. //string whereClause = GeometryColumn + ".STIntersects(geometry::STGeomFromText('" + bboxText + "', " + SRID + ")" + MakeValidString + ") = 1";
  340. string whereClause = String.Format("{0}{1}.STIntersects({4}::STGeomFromText('{2}', {3})) = 1",
  341. GeometryColumn, MakeValidString, bboxText, SRID, _spatialObject);
  342. return whereClause; // strBbox;
  343. }
  344. /// <summary>
  345. /// Returns the features that intersects with 'geom'
  346. /// </summary>
  347. /// <param name="geom"></param>
  348. /// <param name="ds">FeatureDataSet to fill data into</param>
  349. public void ExecuteIntersectionQuery(Geometry geom, FeatureDataSet ds)
  350. {
  351. //List<Geometry> features = new List<Geometry>();
  352. using (SqlConnection conn = new SqlConnection(_connectionString))
  353. {
  354. //TODO: Convert to SQL Server
  355. string strGeom = _spatialObject + "::STGeomFromText('" + geom.AsText() + "', #SRID#)";
  356. strGeom = strGeom.Replace("#SRID#", SRID > 0 ? SRID.ToString() : "0");
  357. strGeom = GeometryColumn + ".STIntersects(" + strGeom + ") = 1";
  358. string strSQL = "SELECT g.* , g." + GeometryColumn + ").STAsBinary() As sharpmap_tempgeometry FROM " + Table + " g WHERE ";
  359. if (!String.IsNullOrEmpty(_defintionQuery))
  360. strSQL += DefinitionQuery + " AND ";
  361. strSQL += strGeom;
  362. using (SqlDataAdapter adapter = new SqlDataAdapter(strSQL, conn))
  363. {
  364. conn.Open();
  365. adapter.Fill(ds);
  366. conn.Close();
  367. if (ds.Tables.Count > 0)
  368. {
  369. FeatureDataTable fdt = new FeatureDataTable(ds.Tables[0]);
  370. foreach (System.Data.DataColumn col in ds.Tables[0].Columns)
  371. if (col.ColumnName != GeometryColumn && col.ColumnName != "sharpmap_tempgeometry")
  372. fdt.Columns.Add(col.ColumnName, col.DataType, col.Expression);
  373. foreach (System.Data.DataRow dr in ds.Tables[0].Rows)
  374. {
  375. FeatureDataRow fdr = fdt.NewRow();
  376. foreach (System.Data.DataColumn col in ds.Tables[0].Columns)
  377. if (col.ColumnName != GeometryColumn && col.ColumnName != "sharpmap_tempgeometry")
  378. fdr[col.ColumnName] = dr[col];
  379. fdr.Geometry = Converters.WellKnownBinary.GeometryFromWKB.Parse((byte[])dr["sharpmap_tempgeometry"]);
  380. fdt.AddRow(fdr);
  381. }
  382. ds.Tables.Add(fdt);
  383. }
  384. }
  385. }
  386. }
  387. /*
  388. /// <summary>
  389. /// Convert WellKnownText to linestrings
  390. /// </summary>
  391. /// <param name="wkt"></param>
  392. /// <returns></returns>
  393. private LineString WktToLineString(string wkt)
  394. {
  395. LineString line = new LineString();
  396. wkt = wkt.Substring(wkt.LastIndexOf('(') + 1).Split(')')[0];
  397. string[] strPoints = wkt.Split(',');
  398. foreach (string strPoint in strPoints)
  399. {
  400. string[] coord = strPoint.Split(' ');
  401. line.Vertices.Add(new Point(double.Parse(coord[0], Map.NumberFormatEnUs), double.Parse(coord[1], Map.NumberFormatEnUs)));
  402. }
  403. return line;
  404. }
  405. */
  406. /// <summary>
  407. /// Returns the number of features in the dataset
  408. /// </summary>
  409. /// <returns>number of features</returns>
  410. public int GetFeatureCount()
  411. {
  412. int count;
  413. using (SqlConnection conn = new SqlConnection(_connectionString))
  414. {
  415. string strSQL = "SELECT COUNT(*) FROM " + Table;
  416. if (!String.IsNullOrEmpty(_defintionQuery))
  417. strSQL += " WHERE " + DefinitionQuery;
  418. using (SqlCommand command = new SqlCommand(strSQL, conn))
  419. {
  420. conn.Open();
  421. count = (int)command.ExecuteScalar();
  422. conn.Close();
  423. }
  424. }
  425. return count;
  426. }
  427. #region IProvider Members
  428. private string _defintionQuery;
  429. /// <summary>
  430. /// Definition query used for limiting dataset
  431. /// </summary>
  432. public string DefinitionQuery
  433. {
  434. get { return _defintionQuery; }
  435. set { _defintionQuery = value; }
  436. }
  437. /// <summary>
  438. /// Gets a collection of columns in the dataset
  439. /// </summary>
  440. public System.Data.DataColumnCollection Columns
  441. {
  442. get {
  443. throw new NotImplementedException();
  444. }
  445. }
  446. private int _srid;
  447. /// <summary>
  448. /// Spacial Reference ID
  449. /// </summary>
  450. public int SRID
  451. {
  452. get {
  453. return _srid;
  454. }
  455. set {
  456. _srid = value;
  457. }
  458. }
  459. /// <summary>
  460. /// Returns a datarow based on a RowID
  461. /// </summary>
  462. /// <param name="rowId"></param>
  463. /// <returns>datarow</returns>
  464. public FeatureDataRow GetFeature(uint rowId)
  465. {
  466. using (SqlConnection conn = new SqlConnection(_connectionString))
  467. {
  468. string strSQL = "select g.* , g." + GeometryColumn + ".STAsBinary() As sharpmap_tempgeometry from " + Table + " g WHERE " + ObjectIdColumn + "=" + rowId + "";
  469. using (SqlDataAdapter adapter = new SqlDataAdapter(strSQL, conn))
  470. {
  471. FeatureDataSet ds = new FeatureDataSet();
  472. conn.Open();
  473. adapter.Fill(ds);
  474. conn.Close();
  475. if (ds.Tables.Count > 0)
  476. {
  477. FeatureDataTable fdt = new FeatureDataTable(ds.Tables[0]);
  478. foreach (System.Data.DataColumn col in ds.Tables[0].Columns)
  479. if (col.ColumnName != GeometryColumn && col.ColumnName != "sharpmap_tempgeometry")
  480. fdt.Columns.Add(col.ColumnName, col.DataType, col.Expression);
  481. if(ds.Tables[0].Rows.Count>0)
  482. {
  483. System.Data.DataRow dr = ds.Tables[0].Rows[0];
  484. FeatureDataRow fdr = fdt.NewRow();
  485. foreach (System.Data.DataColumn col in ds.Tables[0].Columns)
  486. if (col.ColumnName != GeometryColumn && col.ColumnName != "sharpmap_tempgeometry")
  487. fdr[col.ColumnName] = dr[col];
  488. fdr.Geometry = Converters.WellKnownBinary.GeometryFromWKB.Parse((byte[])dr["sharpmap_tempgeometry"]);
  489. return fdr;
  490. }
  491. return null;
  492. }
  493. return null;
  494. }
  495. }
  496. }
  497. /// <summary>
  498. /// Boundingbox of dataset
  499. /// </summary>
  500. /// <returns>boundingbox</returns>
  501. public BoundingBox GetExtents()
  502. {
  503. using (SqlConnection conn = new SqlConnection(_connectionString))
  504. {
  505. //string strSQL = "SELECT g." + GeometryColumn + ".STEnvelope().STAsText() FROM " + Table + " g ";
  506. var strSQL = String.Format("SELECT g.{0}{1}.STEnvelope().STAsText() FROM {2} g ",
  507. GeometryColumn, MakeValidString, Table);
  508. if (!String.IsNullOrEmpty(_defintionQuery))
  509. strSQL += " WHERE " + DefinitionQuery;
  510. using (SqlCommand command = new SqlCommand(strSQL, conn))
  511. {
  512. conn.Open();
  513. //Geometry geom = null;
  514. BoundingBox bx = null;
  515. SqlDataReader dr = command.ExecuteReader();
  516. while (dr.Read())
  517. {
  518. string wkt = dr.GetString(0); //[GeometryColumn];
  519. Geometry g = Converters.WellKnownText.GeometryFromWKT.Parse(wkt);
  520. BoundingBox bb = g.GetBoundingBox();
  521. bx = bx == null ? bb : bx.Join(bb);
  522. }
  523. dr.Close();
  524. conn.Close();
  525. return bx;
  526. }
  527. }
  528. }
  529. /// <summary>
  530. /// Gets the connection ID of the datasource
  531. /// </summary>
  532. public string ConnectionID
  533. {
  534. get { return _connectionString; }
  535. }
  536. #endregion
  537. #region IProvider Members
  538. /// <summary>
  539. /// Returns all features with the view box
  540. /// </summary>
  541. /// <param name="bbox">view box</param>
  542. /// <param name="ds">FeatureDataSet to fill data into</param>
  543. public void ExecuteIntersectionQuery(BoundingBox bbox, FeatureDataSet ds)
  544. {
  545. //List<Geometry> features = new List<Geometry>();
  546. using (SqlConnection conn = new SqlConnection(_connectionString))
  547. {
  548. //Get bounding box string
  549. string strBbox = GetBoxFilterStr(bbox);
  550. //string strSQL = "SELECT g.*, g." + GeometryColumn + ".STAsBinary() AS sharpmap_tempgeometry ";
  551. string strSQL = String.Format(
  552. "SELECT g.*, g.{0}{1}.STAsBinary() AS sharpmap_tempgeometry FROM {2} g WHERE ",
  553. GeometryColumn, MakeValidString, Table);
  554. if (!String.IsNullOrEmpty(_defintionQuery))
  555. strSQL += DefinitionQuery + " AND ";
  556. strSQL += strBbox;
  557. using (SqlDataAdapter adapter = new SqlDataAdapter(strSQL, conn))
  558. {
  559. conn.Open();
  560. System.Data.DataSet ds2 = new System.Data.DataSet();
  561. adapter.Fill(ds2);
  562. conn.Close();
  563. if (ds2.Tables.Count > 0)
  564. {
  565. FeatureDataTable fdt = new FeatureDataTable(ds2.Tables[0]);
  566. foreach (System.Data.DataColumn col in ds2.Tables[0].Columns)
  567. if (col.ColumnName != GeometryColumn && col.ColumnName != "sharpmap_tempgeometry")
  568. fdt.Columns.Add(col.ColumnName,col.DataType,col.Expression);
  569. foreach (System.Data.DataRow dr in ds2.Tables[0].Rows)
  570. {
  571. FeatureDataRow fdr = fdt.NewRow();
  572. foreach(System.Data.DataColumn col in ds2.Tables[0].Columns)
  573. if (col.ColumnName != GeometryColumn && col.ColumnName != "sharpmap_tempgeometry")
  574. fdr[col.ColumnName] = dr[col];
  575. fdr.Geometry = Converters.WellKnownBinary.GeometryFromWKB.Parse((byte[])dr["sharpmap_tempgeometry"]);
  576. fdt.AddRow(fdr);
  577. }
  578. ds.Tables.Add(fdt);
  579. }
  580. }
  581. }
  582. }
  583. #endregion
  584. }
  585. }