1 /****************************************************************************** 2 * 3 * Project: netCDF read/write Driver 4 * Purpose: GDAL bindings over netCDF library. 5 * Author: Winor Chen <wchen329 at wisc.edu> 6 * 7 ****************************************************************************** 8 * Copyright (c) 2019, Winor Chen <wchen329 at wisc.edu> 9 * 10 * Permission is hereby granted, free of charge, to any person obtaining a 11 * copy of this software and associated documentation files (the "Software"), 12 * to deal in the Software without restriction, including without limitation 13 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 * and/or sell copies of the Software, and to permit persons to whom the 15 * Software is furnished to do so, subject to the following conditions: 16 * 17 * The above copyright notice and this permission notice shall be included 18 * in all copies or substantial portions of the Software. 19 * 20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 21 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 23 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 26 * DEALINGS IN THE SOFTWARE. 27 ****************************************************************************/ 28 #ifndef __NETCDFSGWRITERUTIL_H__ 29 #define __NETCDFSGWRITERUTIL_H__ 30 #include <cstdio> 31 #include <queue> 32 #include <typeinfo> 33 #include <vector> 34 #include "cpl_vsi.h" 35 #include "ogr_core.h" 36 #include "ogrsf_frmts.h" 37 #include "netcdflayersg.h" 38 #include "netcdfsg.h" 39 #include "netcdfvirtual.h" 40 41 namespace nccfdriver 42 { 43 44 /* OGR_SGeometry_Feature 45 * Constructs over a OGRFeature 46 * gives some basic information about that SGeometry Feature such as 47 * Hold references... limited to scope of its references 48 * - what's its geometry type 49 * - how much total points it has 50 * - how many parts it has 51 * - a vector of counts of points for each part 52 */ 53 class SGeometry_Feature 54 { 55 bool hasInteriorRing; 56 const OGRGeometry * geometry_ref; 57 geom_t type; 58 size_t total_point_count; 59 size_t total_part_count; 60 std::vector<size_t> ppart_node_count; 61 std::vector<bool> part_at_ind_interior; // for use with Multipolygons ONLY 62 mutable OGRPoint pt_buffer; 63 64 public: getType()65 geom_t getType() { return this->type; } getTotalNodeCount()66 size_t getTotalNodeCount() { return this->total_point_count; } getTotalPartCount()67 size_t getTotalPartCount() { return this->total_part_count; } getPerPartNodeCount()68 std::vector<size_t> & getPerPartNodeCount() { return this->ppart_node_count; } 69 const OGRPoint& getPoint(size_t part_no, int point_index) const; 70 explicit SGeometry_Feature(OGRFeature&); getHasInteriorRing()71 bool getHasInteriorRing() { return this->hasInteriorRing; } IsPartAtIndInteriorRing(size_t ind)72 bool IsPartAtIndInteriorRing(size_t ind) { return this->part_at_ind_interior[ind]; } // ONLY used for Multipolygon 73 }; 74 75 /* A memory buffer with a soft limit 76 * Has basic capability of over quota checking, and memory counting 77 */ 78 class WBuffer 79 { 80 unsigned long long used_mem = 0; 81 82 public: 83 /* addCount(...) 84 * Takes in a size, and directly adds that size to memory count 85 */ 86 void addCount(unsigned long long memuse); 87 88 /* subCount(...) 89 * Directly subtracts the specified size from used_mem 90 */ 91 void subCount(unsigned long long memfree); getUsage()92 unsigned long long& getUsage() { return used_mem; } 93 reset()94 void reset() { this->used_mem = 0; } 95 WBuffer()96 WBuffer() {} 97 }; 98 99 100 101 /* OGR_SGFS_Transaction 102 * Abstract class for a committable transaction 103 * 104 */ 105 class OGR_SGFS_Transaction 106 { 107 int varId = INVALID_VAR_ID; 108 109 public: 110 /* int commit(...); 111 * Arguments: int ncid, the dataset to write to 112 * int write_loc, the index in which to write to 113 * Implementation: should write the transaction to netCDF file 114 * 115 */ 116 virtual void commit(netCDFVID& n, size_t write_loc) = 0; 117 118 /* unsigned long long count(...) 119 * Implementation: supposed to return an approximate count of memory usage 120 * Most classes will implement with sizeof(*this), except if otherwise uncounted for dynamic allocation is involved. 121 */ 122 virtual unsigned long long count() = 0; 123 124 /* appendToLog 125 * Implementation - given a file pointer, a transaction will be written to that log file in the format: 126 * - 127 * transactionVarId - sizeof(int) bytes 128 * NC_TYPE - sizeof(int) bytes 129 * (nc_char only) OP - 1 byte (0 if does not require COUNT or non-zero i.e. 1 if does) 130 * (nc_char only): SIZE of data - sizeof(size_t) bytes 131 * DATA - size depends on NC_TYPE 132 */ 133 virtual void appendToLog(VSILFILE*) = 0; 134 135 /* ~OGR_SGFS_Transaction() 136 * Empty. Simply here to stop the compiler from complaining... 137 */ ~OGR_SGFS_Transaction()138 virtual ~OGR_SGFS_Transaction() {} 139 140 141 /* OGR_SGFS_Transaction() 142 * Empty. Simply here to stop one of the CI machines from complaining... 143 */ OGR_SGFS_Transaction()144 OGR_SGFS_Transaction() {} 145 146 /* void getVarId(...); 147 * Gets the var in which to commit the transaction to. 148 */ getVarId()149 int getVarId() { return this->varId; } 150 151 /* nc_type getType 152 * Returns the type of transaction being saved 153 */ 154 virtual nc_type getType() = 0; 155 156 /* void setVarId(...); 157 * Sets the var in which to commit the transaction to. 158 */ setVarId(int vId)159 void setVarId(int vId) { this->varId = vId; } 160 161 }; 162 163 typedef std::map<int, void*> NCWMap; 164 typedef std::pair<int, void*> NCWEntry; // NC Writer Entry 165 typedef std::unique_ptr<OGR_SGFS_Transaction> MTPtr; // a.k.a Managed Transaction Ptr 166 genericLogAppend(T_c_type r,int vId,VSILFILE * f)167 template<class T_c_type, nc_type T_nc_type> void genericLogAppend(T_c_type r, int vId, VSILFILE* f) 168 { 169 T_c_type rep = r; 170 int varId = vId; 171 int type = T_nc_type; 172 VSIFWriteL(&varId, sizeof(int), 1, f); // write varID data 173 VSIFWriteL(&type, sizeof(int), 1, f); // write NC type 174 VSIFWriteL(&rep, sizeof(T_c_type), 1, f); // write data 175 } 176 genericLogDataRead(int varId,VSILFILE * f)177 template<class T_c_type, class T_r_type> MTPtr genericLogDataRead(int varId, VSILFILE* f) 178 { 179 T_r_type data; 180 if(!VSIFReadL(&data, sizeof(T_r_type), 1, f)) 181 { 182 return MTPtr(nullptr); // invalid read case 183 } 184 return MTPtr(new T_c_type(varId, data)); 185 } 186 187 /* OGR_SGFS_NC_Char_Transaction 188 * Writes to an NC_CHAR variable 189 */ 190 class OGR_SGFS_NC_Char_Transaction : public OGR_SGFS_Transaction 191 { 192 std::string char_rep; 193 194 public: commit(netCDFVID & n,size_t write_loc)195 void commit(netCDFVID& n, size_t write_loc) override { n.nc_put_vvar1_text(OGR_SGFS_Transaction::getVarId(), &write_loc, char_rep.c_str()); } count()196 unsigned long long count() override { return char_rep.size() + sizeof(*this); } // account for actual character representation, this class 197 void appendToLog(VSILFILE* f) override; getType()198 nc_type getType() override { return NC_CHAR; } OGR_SGFS_NC_Char_Transaction(int i_varId,const char * pszVal)199 OGR_SGFS_NC_Char_Transaction(int i_varId, const char* pszVal) : 200 char_rep(pszVal) 201 { 202 OGR_SGFS_Transaction::setVarId(i_varId); 203 } 204 }; 205 206 /* OGR_SGFS_NC_CharA_Transaction 207 * Writes to an NC_CHAR variable, using vara instead of var1 208 * Used to store 2D character array values, specifically 209 */ 210 class OGR_SGFS_NC_CharA_Transaction : public OGR_SGFS_Transaction 211 { 212 std::string char_rep; 213 size_t counts[2]; 214 215 public: commit(netCDFVID & n,size_t write_loc)216 void commit(netCDFVID& n, size_t write_loc) override { size_t ind[2] = {write_loc, 0}; n.nc_put_vvara_text(OGR_SGFS_Transaction::getVarId(), ind, counts, char_rep.c_str()); } count()217 unsigned long long count() override { return char_rep.size() + sizeof(*this); } // account for actual character representation, this class 218 void appendToLog(VSILFILE* f) override; getType()219 nc_type getType() override { return NC_CHAR; } OGR_SGFS_NC_CharA_Transaction(int i_varId,const char * pszVal)220 OGR_SGFS_NC_CharA_Transaction(int i_varId, const char* pszVal) : 221 char_rep(pszVal), 222 counts{1, char_rep.length()} 223 { 224 OGR_SGFS_Transaction::setVarId(i_varId); 225 } 226 }; 227 228 template <class VClass, nc_type ntype> class OGR_SGFS_NC_Transaction_Generic : public OGR_SGFS_Transaction 229 { 230 VClass rep; 231 232 public: commit(netCDFVID & n,size_t write_loc)233 void commit(netCDFVID& n, size_t write_loc) override 234 { 235 n.nc_put_vvar_generic<VClass>(OGR_SGFS_Transaction::getVarId(), &write_loc, &rep); 236 } 237 count()238 unsigned long long count() override { return sizeof(*this); } appendToLog(VSILFILE * f)239 void appendToLog(VSILFILE* f) override 240 { 241 genericLogAppend<VClass, ntype>(rep, OGR_SGFS_Transaction::getVarId(), f); 242 } 243 OGR_SGFS_NC_Transaction_Generic(int i_varId,VClass in)244 OGR_SGFS_NC_Transaction_Generic(int i_varId, VClass in) : 245 rep(in) 246 { 247 OGR_SGFS_Transaction::setVarId(i_varId); 248 } 249 getData()250 VClass getData() 251 { 252 return rep; 253 } 254 getType()255 nc_type getType() override { return ntype; } 256 }; 257 258 typedef OGR_SGFS_NC_Transaction_Generic<signed char, NC_BYTE> OGR_SGFS_NC_Byte_Transaction; 259 typedef OGR_SGFS_NC_Transaction_Generic<short, NC_SHORT> OGR_SGFS_NC_Short_Transaction; 260 typedef OGR_SGFS_NC_Transaction_Generic<int, NC_INT> OGR_SGFS_NC_Int_Transaction; 261 typedef OGR_SGFS_NC_Transaction_Generic<float, NC_FLOAT> OGR_SGFS_NC_Float_Transaction; 262 typedef OGR_SGFS_NC_Transaction_Generic<double, NC_DOUBLE> OGR_SGFS_NC_Double_Transaction; 263 264 #ifdef NETCDF_HAS_NC4 265 typedef OGR_SGFS_NC_Transaction_Generic<unsigned, NC_UINT> OGR_SGFS_NC_UInt_Transaction; 266 typedef OGR_SGFS_NC_Transaction_Generic<unsigned long long, NC_UINT64> OGR_SGFS_NC_UInt64_Transaction; 267 typedef OGR_SGFS_NC_Transaction_Generic<long long, NC_INT64> OGR_SGFS_NC_Int64_Transaction; 268 typedef OGR_SGFS_NC_Transaction_Generic<unsigned char, NC_UBYTE> OGR_SGFS_NC_UByte_Transaction; 269 typedef OGR_SGFS_NC_Transaction_Generic<unsigned short, NC_USHORT> OGR_SGFS_NC_UShort_Transaction; 270 271 /* OGR_SGFS_NC_String_Transaction 272 * Writes to an NC_STRING variable, in a similar manner as NC_Char 273 */ 274 class OGR_SGFS_NC_String_Transaction : public OGR_SGFS_Transaction 275 { 276 std::string char_rep; 277 278 public: commit(netCDFVID & n,size_t write_loc)279 void commit(netCDFVID& n, size_t write_loc) override 280 { 281 const char * writable = char_rep.c_str(); 282 n.nc_put_vvar1_string(OGR_SGFS_Transaction::getVarId(), &write_loc, &(writable)); 283 } 284 count()285 unsigned long long count() override { return char_rep.size() + sizeof(*this); } // account for actual character representation, this class 286 getType()287 nc_type getType() override { return NC_STRING; } 288 289 void appendToLog(VSILFILE* f) override; 290 OGR_SGFS_NC_String_Transaction(int i_varId,const char * pszVal)291 OGR_SGFS_NC_String_Transaction(int i_varId, const char* pszVal) : 292 char_rep(pszVal) 293 { 294 OGR_SGFS_Transaction::setVarId(i_varId); 295 } 296 }; 297 298 #endif 299 300 /* WTransactionLog 301 * - 302 * A temporary file which contains transactions to be written to a netCDF file. 303 * Once the transaction log is created it is set on write mode, it can only be read to after startRead() is called 304 */ 305 class WTransactionLog 306 { 307 bool readMode = false; 308 std::string wlogName; // name of the temporary file, should be unique 309 VSILFILE* log = nullptr; 310 311 312 WTransactionLog(WTransactionLog&); // avoid possible undefined behavior 313 WTransactionLog operator=(const WTransactionLog&); 314 315 public: logIsNull()316 bool logIsNull() { return log == nullptr; } 317 void startLog(); // always call this first to open the file 318 void startRead(); // then call this before reading it 319 void push(MTPtr); 320 321 // read mode 322 MTPtr pop(); // to test for EOF, test to see if pointer returned is null ptr 323 324 // construction, destruction 325 explicit WTransactionLog(const std::string& logName); 326 ~WTransactionLog(); 327 }; 328 329 /* OGR_NCScribe 330 * Buffers several netCDF transactions in memory or in a log. 331 * General scribe class 332 */ 333 class OGR_NCScribe 334 { 335 netCDFVID & ncvd; 336 WBuffer buf; 337 WTransactionLog wl; 338 bool singleDatumMode = false; 339 340 std::queue<MTPtr> transactionQueue; 341 std::map<int, size_t> varWriteInds; 342 std::map<int, size_t> varMaxInds; 343 344 public: 345 /* size_t getWriteCount() 346 * Return the total write count (happened + pending) of certain variable 347 */ getWriteCount(int varId)348 size_t getWriteCount(int varId) { return this->varMaxInds.at(varId); } 349 350 /* void commit_transaction() 351 * Replays all transactions to disk (according to fs stipulations) 352 */ 353 void commit_transaction(); 354 355 /* MTPtr pop() 356 * Get the next transaction, if it exists. 357 * If not, it will just return a shared_ptr with nullptr inside 358 */ 359 MTPtr pop(); 360 361 /* void log_transacion() 362 * Saves the current queued transactions to a log. 363 */ 364 void log_transaction(); 365 366 /* void enqueue_transaction() 367 * Add a transaction to perform 368 * Once a transaction is enqueued, it will only be dequeued on commit 369 */ 370 void enqueue_transaction(MTPtr transactionAdd); 371 getMemBuffer()372 WBuffer& getMemBuffer() { return buf; } 373 374 /* OGR_SGeometry_Field_Scribe() 375 * Constructs a Field Scribe over a dataset 376 */ OGR_NCScribe(netCDFVID & ncd,const std::string & name)377 OGR_NCScribe(netCDFVID& ncd, const std::string& name) : 378 ncvd(ncd), 379 wl(name) 380 {} 381 382 /* setSingleDatumMode(...) 383 * Enables or disables single datum mode 384 * DO NOT use this when a commit is taking place, otherwise 385 * corruption may occur... 386 */ setSingleDatumMode(bool sdm)387 void setSingleDatumMode(bool sdm) { this->singleDatumMode = sdm; } 388 389 }; 390 391 class ncLayer_SG_Metadata 392 { 393 int & ncID; // ncid REF. which tracks ncID changes that may be made upstream 394 395 netCDFVID& vDataset; 396 OGR_NCScribe & ncb; 397 geom_t writableType = NONE; 398 std::string containerVarName; 399 int containerVar_realID = INVALID_VAR_ID; 400 bool interiorRingDetected = false; // flips on when an interior ring polygon has been detected 401 std::vector<int> node_coordinates_varIDs;// ids in X, Y (and then possibly Z) order 402 int node_coordinates_dimID = INVALID_DIM_ID; // dim of all node_coordinates 403 int node_count_dimID = INVALID_DIM_ID; // node count dim 404 int node_count_varID = INVALID_DIM_ID; 405 int pnc_dimID = INVALID_DIM_ID; // part node count dim AND interior ring dim 406 int pnc_varID = INVALID_VAR_ID; 407 int intring_varID = INVALID_VAR_ID; 408 size_t next_write_pos_node_coord = 0; 409 size_t next_write_pos_node_count = 0; 410 size_t next_write_pos_pnc = 0; 411 412 public: getWritableType()413 geom_t getWritableType() { return this->writableType; } 414 void writeSGeometryFeature(SGeometry_Feature& ft); get_containerRealID()415 int get_containerRealID() { return this->containerVar_realID; } get_containerName()416 std::string get_containerName() { return this->containerVarName; } get_node_count_dimID()417 int get_node_count_dimID() { return this->node_count_dimID; } get_node_coord_dimID()418 int get_node_coord_dimID() { return this->node_coordinates_dimID; } get_pnc_dimID()419 int get_pnc_dimID() { return this->pnc_dimID; } get_pnc_varID()420 int get_pnc_varID() { return this->pnc_varID; } get_intring_varID()421 int get_intring_varID() { return this->intring_varID; } get_nodeCoordVarIDs()422 std::vector<int>& get_nodeCoordVarIDs() { return this->node_coordinates_varIDs; } get_next_write_pos_node_coord()423 size_t get_next_write_pos_node_coord() { return this->next_write_pos_node_coord; } get_next_write_pos_node_count()424 size_t get_next_write_pos_node_count() { return this->next_write_pos_node_count; } get_next_write_pos_pnc()425 size_t get_next_write_pos_pnc() { return this->next_write_pos_pnc; } getInteriorRingDetected()426 bool getInteriorRingDetected() { return this->interiorRingDetected; } 427 void initializeNewContainer(int containerVID); 428 ncLayer_SG_Metadata(int & i_ncID, geom_t geo, netCDFVID& ncdf, OGR_NCScribe& scribe); 429 }; 430 431 /* WBufferManager 432 * - 433 * Simply takes a collection of buffers in and a quota limit and sums all the usages up 434 * to establish if buffers are over the soft limit (collectively) 435 * 436 * The buffers added, however, are not memory managed by WBufferManager 437 */ 438 class WBufferManager 439 { 440 unsigned long long buffer_soft_limit = 0; 441 std::vector<WBuffer*> bufs; 442 443 public: 444 bool isOverQuota(); adjustLimit(unsigned long long lim)445 void adjustLimit(unsigned long long lim) { this->buffer_soft_limit = lim; } addBuffer(WBuffer * b)446 void addBuffer(WBuffer* b) { this->bufs.push_back(b); } WBufferManager(unsigned long long lim)447 explicit WBufferManager(unsigned long long lim) : buffer_soft_limit(lim){ } 448 }; 449 450 451 // Exception Classes 452 class SGWriter_Exception : public SG_Exception 453 { 454 public: get_err_msg()455 const char * get_err_msg() override { return "A general error occurred when writing a netCDF dataset"; } 456 }; 457 458 class SGWriter_Exception_NCWriteFailure : public SGWriter_Exception 459 { 460 std::string msg; 461 462 public: get_err_msg()463 const char * get_err_msg() override { return this->msg.c_str(); } 464 SGWriter_Exception_NCWriteFailure(const char * layer_name, const char * failure_name, 465 const char * failure_type); 466 }; 467 468 class SGWriter_Exception_NCInqFailure : public SGWriter_Exception 469 { 470 std::string msg; 471 472 public: get_err_msg()473 const char * get_err_msg() override { return this->msg.c_str(); } 474 SGWriter_Exception_NCInqFailure(const char * layer_name, const char * failure_name, 475 const char * failure_type); 476 }; 477 478 class SGWriter_Exception_NCDefFailure : public SGWriter_Exception 479 { 480 std::string msg; 481 482 public: get_err_msg()483 const char * get_err_msg() override { return this->msg.c_str(); } 484 SGWriter_Exception_NCDefFailure(const char * layer_name, const char * failure_name, 485 const char * failure_type); 486 }; 487 488 class SGWriter_Exception_EmptyGeometry : public SGWriter_Exception 489 { 490 std::string msg; 491 492 public: get_err_msg()493 const char * get_err_msg() override { return this->msg.c_str(); } SGWriter_Exception_EmptyGeometry()494 SGWriter_Exception_EmptyGeometry() : msg("An empty geometry was detected when writing a netCDF file. Empty geometries are not allowed.") {} 495 }; 496 497 class SGWriter_Exception_RingOOB: public SGWriter_Exception 498 { 499 std::string msg; 500 501 public: get_err_msg()502 const char * get_err_msg() override { return this->msg.c_str(); } SGWriter_Exception_RingOOB()503 SGWriter_Exception_RingOOB() : msg("An attempt was made to read a polygon ring that does not exist.") {} 504 }; 505 506 class SGWriter_Exception_NCDelFailure : public SGWriter_Exception 507 { 508 std::string msg; 509 510 public: get_err_msg()511 const char * get_err_msg() override { return this->msg.c_str(); } SGWriter_Exception_NCDelFailure(const char * layer,const char * what)512 SGWriter_Exception_NCDelFailure(const char* layer, const char* what) 513 : msg("[" + std::string(layer) + "] Failed to delete: " + std::string(what)) 514 {} 515 }; 516 517 // Helper Functions that for writing 518 519 /* std::vector<..> writeGeometryContainer(...) 520 * Writes a geometry container of a given geometry type given the following arguments: 521 * int ncID - ncid as used in netcdf.h, group or file id 522 * std::string name - what to name this container 523 * geom_t geometry_type - the geometry type of the container 524 * std::vector<..> node_coordinate_names - variable names corresponding to each axis 525 * Only writes attributes that are for sure required. i.e. does NOT required interior ring for anything or part node count for Polygons 526 * 527 * Returns: geometry container variable ID 528 */ 529 int write_Geometry_Container 530 (int ncID, const std::string& name, geom_t geometry_type, const std::vector<std::string> & node_coordinate_names); 531 NCWMapAllocIfNeeded(int varid,NCWMap & mapAdd,size_t numEntries,std::vector<int> & v)532 template<class W_type> inline void NCWMapAllocIfNeeded(int varid, NCWMap& mapAdd, size_t numEntries, std::vector<int> & v) 533 { 534 if(mapAdd.count(varid) < 1) 535 { 536 mapAdd.insert(NCWEntry(varid, CPLMalloc(sizeof(W_type) * numEntries))); 537 v.push_back(varid); 538 } 539 } 540 NCWMapWriteAndCommit(int varid,NCWMap & mapAdd,size_t currentEntry,size_t numEntries,W_type data,netCDFVID & vcdf)541 template<class W_type> inline void NCWMapWriteAndCommit(int varid, NCWMap& mapAdd, size_t currentEntry, size_t numEntries, W_type data, netCDFVID& vcdf) 542 { 543 W_type* ptr = static_cast<W_type*>(mapAdd.at(varid)); 544 ptr[currentEntry] = data; 545 static const size_t BEGIN = 0; 546 547 // If all items are ready, write the array, and free it, delete the pointer 548 if (currentEntry == (numEntries - 1)) 549 { 550 try 551 { 552 // Write the whole array at once 553 vcdf.nc_put_vvara_generic<W_type>(varid, &BEGIN, &numEntries, ptr); 554 } 555 catch (SG_Exception_VWrite_Failure& e) 556 { 557 CPLError(CE_Warning, CPLE_FileIO, "%s", e.get_err_msg()); 558 } 559 560 CPLFree(mapAdd.at(varid)); 561 mapAdd.erase(varid); 562 } 563 } 564 } 565 566 #endif 567