/src/gdal/ogr/ogrsf_frmts/mitab/mitab_mapindexblock.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /********************************************************************** |
2 | | * |
3 | | * Name: mitab_mapindexblock.cpp |
4 | | * Project: MapInfo TAB Read/Write library |
5 | | * Language: C++ |
6 | | * Purpose: Implementation of the TABMAPIndexBlock class used to handle |
7 | | * reading/writing of the .MAP files' index blocks |
8 | | * Author: Daniel Morissette, dmorissette@dmsolutions.ca |
9 | | * |
10 | | ********************************************************************** |
11 | | * Copyright (c) 1999, 2000, Daniel Morissette |
12 | | * Copyright (c) 2014, Even Rouault <even.rouault at spatialys.com> |
13 | | * |
14 | | * SPDX-License-Identifier: MIT |
15 | | **********************************************************************/ |
16 | | |
17 | | #include "cpl_port.h" |
18 | | #include "mitab.h" |
19 | | |
20 | | #include <cmath> |
21 | | #include <cstdlib> |
22 | | #include <cstring> |
23 | | |
24 | | #include <algorithm> |
25 | | |
26 | | #include "cpl_conv.h" |
27 | | #include "cpl_error.h" |
28 | | #include "cpl_vsi.h" |
29 | | #include "mitab_priv.h" |
30 | | |
31 | | /*===================================================================== |
32 | | * class TABMAPIndexBlock |
33 | | *====================================================================*/ |
34 | | |
35 | | /********************************************************************** |
36 | | * TABMAPIndexBlock::TABMAPIndexBlock() |
37 | | * |
38 | | * Constructor. |
39 | | **********************************************************************/ |
40 | | TABMAPIndexBlock::TABMAPIndexBlock(TABAccess eAccessMode /*= TABRead*/) |
41 | 106 | : TABRawBinBlock(eAccessMode, TRUE), m_numEntries(0), m_nMinX(1000000000), |
42 | 106 | m_nMinY(1000000000), m_nMaxX(-1000000000), m_nMaxY(-1000000000), |
43 | 106 | m_poBlockManagerRef(nullptr), m_nCurChildIndex(-1), m_poParentRef(nullptr) |
44 | 106 | { |
45 | 106 | memset(m_asEntries, 0, sizeof(m_asEntries)); |
46 | 106 | } |
47 | | |
48 | | /********************************************************************** |
49 | | * TABMAPIndexBlock::~TABMAPIndexBlock() |
50 | | * |
51 | | * Destructor. |
52 | | **********************************************************************/ |
53 | | TABMAPIndexBlock::~TABMAPIndexBlock() |
54 | 106 | { |
55 | 106 | UnsetCurChild(); |
56 | 106 | } |
57 | | |
58 | | /********************************************************************** |
59 | | * TABMAPIndexBlock::UnsetCurChild() |
60 | | **********************************************************************/ |
61 | | |
62 | | void TABMAPIndexBlock::UnsetCurChild() |
63 | 106 | { |
64 | 106 | if (m_poCurChild) |
65 | 19 | { |
66 | 19 | if (m_eAccess == TABWrite || m_eAccess == TABReadWrite) |
67 | 19 | m_poCurChild->CommitToFile(); |
68 | 19 | m_poCurChild.reset(); |
69 | 19 | } |
70 | 106 | m_nCurChildIndex = -1; |
71 | 106 | } |
72 | | |
73 | | /********************************************************************** |
74 | | * TABMAPIndexBlock::InitBlockFromData() |
75 | | * |
76 | | * Perform some initialization on the block after its binary data has |
77 | | * been set or changed (or loaded from a file). |
78 | | * |
79 | | * Returns 0 if successful or -1 if an error happened, in which case |
80 | | * CPLError() will have been called. |
81 | | **********************************************************************/ |
82 | | int TABMAPIndexBlock::InitBlockFromData(GByte *pabyBuf, int nBlockSize, |
83 | | int nSizeUsed, |
84 | | GBool bMakeCopy /* = TRUE */, |
85 | | VSILFILE *fpSrc /* = NULL */, |
86 | | int nOffset /* = 0 */) |
87 | 58 | { |
88 | | /*----------------------------------------------------------------- |
89 | | * First of all, we must call the base class' InitBlockFromData() |
90 | | *----------------------------------------------------------------*/ |
91 | 58 | const int nStatus = TABRawBinBlock::InitBlockFromData( |
92 | 58 | pabyBuf, nBlockSize, nSizeUsed, bMakeCopy, fpSrc, nOffset); |
93 | 58 | if (nStatus != 0) |
94 | 0 | return nStatus; |
95 | | |
96 | | /*----------------------------------------------------------------- |
97 | | * Validate block type |
98 | | *----------------------------------------------------------------*/ |
99 | 58 | if (m_nBlockType != TABMAP_INDEX_BLOCK) |
100 | 0 | { |
101 | 0 | CPLError(CE_Failure, CPLE_FileIO, |
102 | 0 | "InitBlockFromData(): Invalid Block Type: got %d expected %d", |
103 | 0 | m_nBlockType, TABMAP_INDEX_BLOCK); |
104 | 0 | CPLFree(m_pabyBuf); |
105 | 0 | m_pabyBuf = nullptr; |
106 | 0 | return -1; |
107 | 0 | } |
108 | | |
109 | | /*----------------------------------------------------------------- |
110 | | * Init member variables |
111 | | *----------------------------------------------------------------*/ |
112 | 58 | GotoByteInBlock(0x002); |
113 | 58 | m_numEntries = ReadInt16(); |
114 | | |
115 | 58 | if (m_numEntries > 0) |
116 | 58 | ReadAllEntries(); |
117 | | |
118 | 58 | return 0; |
119 | 58 | } |
120 | | |
121 | | /********************************************************************** |
122 | | * TABMAPIndexBlock::CommitToFile() |
123 | | * |
124 | | * Commit the current state of the binary block to the file to which |
125 | | * it has been previously attached. |
126 | | * |
127 | | * This method makes sure all values are properly set in the map object |
128 | | * block header and then calls TABRawBinBlock::CommitToFile() to do |
129 | | * the actual writing to disk. |
130 | | * |
131 | | * Returns 0 if successful or -1 if an error happened, in which case |
132 | | * CPLError() will have been called. |
133 | | **********************************************************************/ |
134 | | int TABMAPIndexBlock::CommitToFile() |
135 | 126 | { |
136 | 126 | if (m_pabyBuf == nullptr) |
137 | 0 | { |
138 | 0 | CPLError(CE_Failure, CPLE_AssertionFailed, |
139 | 0 | "CommitToFile(): Block has not been initialized yet!"); |
140 | 0 | return -1; |
141 | 0 | } |
142 | | |
143 | | /*----------------------------------------------------------------- |
144 | | * Commit child first |
145 | | *----------------------------------------------------------------*/ |
146 | 126 | if (m_poCurChild) |
147 | 20 | { |
148 | 20 | if (m_poCurChild->CommitToFile() != 0) |
149 | 0 | return -1; |
150 | 20 | } |
151 | | |
152 | | /*----------------------------------------------------------------- |
153 | | * Nothing to do here if block has not been modified |
154 | | *----------------------------------------------------------------*/ |
155 | 126 | if (!m_bModified) |
156 | 20 | return 0; |
157 | | |
158 | | /*----------------------------------------------------------------- |
159 | | * Make sure 4 bytes block header is up to date. |
160 | | *----------------------------------------------------------------*/ |
161 | 106 | GotoByteInBlock(0x000); |
162 | | |
163 | 106 | WriteInt16(TABMAP_INDEX_BLOCK); // Block type code |
164 | 106 | WriteInt16(static_cast<GInt16>(m_numEntries)); |
165 | | |
166 | 106 | int nStatus = CPLGetLastErrorType() == CE_Failure ? -1 : 0; |
167 | | |
168 | | /*----------------------------------------------------------------- |
169 | | * Loop through all entries, writing each of them, and calling |
170 | | * CommitToFile() (recursively) on any child index entries we may |
171 | | * encounter. |
172 | | *----------------------------------------------------------------*/ |
173 | 1.35k | for (int i = 0; nStatus == 0 && i < m_numEntries; i++) |
174 | 1.24k | { |
175 | 1.24k | nStatus = WriteNextEntry(&(m_asEntries[i])); |
176 | 1.24k | } |
177 | | |
178 | | /*----------------------------------------------------------------- |
179 | | * OK, call the base class to write the block to disk. |
180 | | *----------------------------------------------------------------*/ |
181 | 106 | if (nStatus == 0) |
182 | 106 | { |
183 | | #ifdef DEBUG_VERBOSE |
184 | | CPLDebug("MITAB", "Committing INDEX block to offset %d", m_nFileOffset); |
185 | | #endif |
186 | 106 | nStatus = TABRawBinBlock::CommitToFile(); |
187 | 106 | } |
188 | | |
189 | 106 | return nStatus; |
190 | 126 | } |
191 | | |
192 | | /********************************************************************** |
193 | | * TABMAPIndexBlock::InitNewBlock() |
194 | | * |
195 | | * Initialize a newly created block so that it knows to which file it |
196 | | * is attached, its block size, etc . and then perform any specific |
197 | | * initialization for this block type, including writing a default |
198 | | * block header, etc. and leave the block ready to receive data. |
199 | | * |
200 | | * This is an alternative to calling ReadFromFile() or InitBlockFromData() |
201 | | * that puts the block in a stable state without loading any initial |
202 | | * data in it. |
203 | | * |
204 | | * Returns 0 if successful or -1 if an error happened, in which case |
205 | | * CPLError() will have been called. |
206 | | **********************************************************************/ |
207 | | int TABMAPIndexBlock::InitNewBlock(VSILFILE *fpSrc, int nBlockSize, |
208 | | int nFileOffset /* = 0*/) |
209 | 48 | { |
210 | | /*----------------------------------------------------------------- |
211 | | * Start with the default initialization |
212 | | *----------------------------------------------------------------*/ |
213 | 48 | if (TABRawBinBlock::InitNewBlock(fpSrc, nBlockSize, nFileOffset) != 0) |
214 | 0 | return -1; |
215 | | |
216 | | /*----------------------------------------------------------------- |
217 | | * And then set default values for the block header. |
218 | | *----------------------------------------------------------------*/ |
219 | 48 | m_numEntries = 0; |
220 | | |
221 | 48 | m_nMinX = 1000000000; |
222 | 48 | m_nMinY = 1000000000; |
223 | 48 | m_nMaxX = -1000000000; |
224 | 48 | m_nMaxY = -1000000000; |
225 | | |
226 | 48 | if (m_eAccess != TABRead && nFileOffset != 0) |
227 | 48 | { |
228 | 48 | GotoByteInBlock(0x000); |
229 | | |
230 | 48 | WriteInt16(TABMAP_INDEX_BLOCK); // Block type code |
231 | 48 | WriteInt16(0); // num. index entries |
232 | 48 | } |
233 | | |
234 | 48 | if (CPLGetLastErrorType() == CE_Failure) |
235 | 0 | return -1; |
236 | | |
237 | 48 | return 0; |
238 | 48 | } |
239 | | |
240 | | /********************************************************************** |
241 | | * TABMAPIndexBlock::ReadNextEntry() |
242 | | * |
243 | | * Read the next index entry from the block and fill the sEntry |
244 | | * structure. |
245 | | * |
246 | | * Returns 0 if successful or -1 if we reached the end of the block. |
247 | | **********************************************************************/ |
248 | | int TABMAPIndexBlock::ReadNextEntry(TABMAPIndexEntry *psEntry) |
249 | 1.11k | { |
250 | 1.11k | if (m_nCurPos < 4) |
251 | 0 | GotoByteInBlock(0x004); |
252 | | |
253 | 1.11k | if (m_nCurPos > 4 + (20 * m_numEntries)) |
254 | 0 | { |
255 | | // End of BLock |
256 | 0 | return -1; |
257 | 0 | } |
258 | | |
259 | 1.11k | psEntry->XMin = ReadInt32(); |
260 | 1.11k | psEntry->YMin = ReadInt32(); |
261 | 1.11k | psEntry->XMax = ReadInt32(); |
262 | 1.11k | psEntry->YMax = ReadInt32(); |
263 | 1.11k | psEntry->nBlockPtr = ReadInt32(); |
264 | | |
265 | 1.11k | if (CPLGetLastErrorType() == CE_Failure) |
266 | 0 | return -1; |
267 | | |
268 | 1.11k | return 0; |
269 | 1.11k | } |
270 | | |
271 | | /********************************************************************** |
272 | | * TABMAPIndexBlock::ReadAllEntries() |
273 | | * |
274 | | * Init the block by reading all entries from the data block. |
275 | | * |
276 | | * Returns 0 if successful or -1 on error. |
277 | | **********************************************************************/ |
278 | | int TABMAPIndexBlock::ReadAllEntries() |
279 | 58 | { |
280 | 58 | CPLAssert(m_numEntries <= GetMaxEntries()); |
281 | 58 | if (m_numEntries == 0) |
282 | 0 | return 0; |
283 | | |
284 | 58 | if (GotoByteInBlock(0x004) != 0) |
285 | 0 | return -1; |
286 | | |
287 | 1.17k | for (int i = 0; i < m_numEntries; i++) |
288 | 1.11k | { |
289 | 1.11k | if (ReadNextEntry(&(m_asEntries[i])) != 0) |
290 | 0 | return -1; |
291 | 1.11k | } |
292 | | |
293 | 58 | return 0; |
294 | 58 | } |
295 | | |
296 | | /********************************************************************** |
297 | | * TABMAPIndexBlock::WriteNextEntry() |
298 | | * |
299 | | * Write the sEntry index entry at current position in the block. |
300 | | * |
301 | | * Returns 0 if successful or -1 if we reached the end of the block. |
302 | | **********************************************************************/ |
303 | | int TABMAPIndexBlock::WriteNextEntry(TABMAPIndexEntry *psEntry) |
304 | 1.24k | { |
305 | 1.24k | if (m_nCurPos < 4) |
306 | 0 | GotoByteInBlock(0x004); |
307 | | |
308 | 1.24k | WriteInt32(psEntry->XMin); |
309 | 1.24k | WriteInt32(psEntry->YMin); |
310 | 1.24k | WriteInt32(psEntry->XMax); |
311 | 1.24k | WriteInt32(psEntry->YMax); |
312 | 1.24k | WriteInt32(psEntry->nBlockPtr); |
313 | | |
314 | 1.24k | if (CPLGetLastErrorType() == CE_Failure) |
315 | 0 | return -1; |
316 | | |
317 | 1.24k | return 0; |
318 | 1.24k | } |
319 | | |
320 | | /********************************************************************** |
321 | | * TABMAPIndexBlock::GetNumFreeEntries() |
322 | | * |
323 | | * Return the number of available entries in this block. |
324 | | * |
325 | | * __TODO__ This function could eventually be improved to search |
326 | | * children leaves as well. |
327 | | **********************************************************************/ |
328 | | int TABMAPIndexBlock::GetNumFreeEntries() |
329 | 1.22k | { |
330 | 1.22k | return (m_nBlockSize - 4) / 20 - m_numEntries; |
331 | 1.22k | } |
332 | | |
333 | | /********************************************************************** |
334 | | * TABMAPIndexBlock::GetEntry() |
335 | | * |
336 | | * Fetch a reference to the requested entry. |
337 | | * |
338 | | * @param iIndex index of entry, must be from 0 to GetNumEntries()-1. |
339 | | * |
340 | | * @return a reference to the internal copy of the entry, or NULL if out |
341 | | * of range. |
342 | | **********************************************************************/ |
343 | | TABMAPIndexEntry *TABMAPIndexBlock::GetEntry(int iIndex) |
344 | 0 | { |
345 | 0 | if (iIndex < 0 || iIndex >= m_numEntries) |
346 | 0 | return nullptr; |
347 | | |
348 | 0 | return m_asEntries + iIndex; |
349 | 0 | } |
350 | | |
351 | | /********************************************************************** |
352 | | * TABMAPIndexBlock::GetCurMaxDepth() |
353 | | * |
354 | | * Return maximum depth in the currently loaded part of the index tree |
355 | | **********************************************************************/ |
356 | | int TABMAPIndexBlock::GetCurMaxDepth() |
357 | 158 | { |
358 | 158 | if (m_poCurChild) |
359 | 62 | return m_poCurChild->GetCurMaxDepth() + 1; |
360 | | |
361 | 96 | return 1; /* No current child... this node counts for one. */ |
362 | 158 | } |
363 | | |
364 | | /********************************************************************** |
365 | | * TABMAPIndexBlock::GetMBR() |
366 | | * |
367 | | * Return the MBR for the current block. |
368 | | **********************************************************************/ |
369 | | void TABMAPIndexBlock::GetMBR(GInt32 &nXMin, GInt32 &nYMin, GInt32 &nXMax, |
370 | | GInt32 &nYMax) |
371 | 2.09k | { |
372 | 2.09k | nXMin = m_nMinX; |
373 | 2.09k | nYMin = m_nMinY; |
374 | 2.09k | nXMax = m_nMaxX; |
375 | 2.09k | nYMax = m_nMaxY; |
376 | 2.09k | } |
377 | | |
378 | | /********************************************************************** |
379 | | * TABMAPIndexBlock::SetMBR() |
380 | | * |
381 | | **********************************************************************/ |
382 | | void TABMAPIndexBlock::SetMBR(GInt32 nXMin, GInt32 nYMin, GInt32 nXMax, |
383 | | GInt32 nYMax) |
384 | 0 | { |
385 | 0 | m_nMinX = nXMin; |
386 | 0 | m_nMinY = nYMin; |
387 | 0 | m_nMaxX = nXMax; |
388 | 0 | m_nMaxY = nYMax; |
389 | 0 | } |
390 | | |
391 | | /********************************************************************** |
392 | | * TABMAPIndexBlock::InsertEntry() |
393 | | * |
394 | | * Add a new entry to this index block. It is assumed that there is at |
395 | | * least one free slot available, so if the block has to be split then it |
396 | | * should have been done prior to calling this function. |
397 | | * |
398 | | * Returns 0 on success, -1 on error. |
399 | | **********************************************************************/ |
400 | | int TABMAPIndexBlock::InsertEntry(GInt32 nXMin, GInt32 nYMin, GInt32 nXMax, |
401 | | GInt32 nYMax, GInt32 nBlockPtr) |
402 | 1.10k | { |
403 | 1.10k | if (m_eAccess != TABWrite && m_eAccess != TABReadWrite) |
404 | 0 | { |
405 | 0 | CPLError( |
406 | 0 | CE_Failure, CPLE_AssertionFailed, |
407 | 0 | "Failed adding index entry: File not opened for write access."); |
408 | 0 | return -1; |
409 | 0 | } |
410 | | |
411 | 1.10k | if (GetNumFreeEntries() < 1) |
412 | 0 | { |
413 | 0 | CPLError(CE_Failure, CPLE_AssertionFailed, |
414 | 0 | "Current Block Index is full, cannot add new entry."); |
415 | 0 | return -1; |
416 | 0 | } |
417 | | |
418 | | /*----------------------------------------------------------------- |
419 | | * Update count of entries and store new entry. |
420 | | *----------------------------------------------------------------*/ |
421 | 1.10k | m_numEntries++; |
422 | 1.10k | CPLAssert(m_numEntries <= GetMaxEntries()); |
423 | | |
424 | 1.10k | m_asEntries[m_numEntries - 1].XMin = nXMin; |
425 | 1.10k | m_asEntries[m_numEntries - 1].YMin = nYMin; |
426 | 1.10k | m_asEntries[m_numEntries - 1].XMax = nXMax; |
427 | 1.10k | m_asEntries[m_numEntries - 1].YMax = nYMax; |
428 | 1.10k | m_asEntries[m_numEntries - 1].nBlockPtr = nBlockPtr; |
429 | | |
430 | 1.10k | m_bModified = TRUE; |
431 | | |
432 | 1.10k | return 0; |
433 | 1.10k | } |
434 | | |
435 | | /********************************************************************** |
436 | | * TABMAPIndexBlock::ChooseSubEntryForInsert() |
437 | | * |
438 | | * Select the entry in this index block in which the new entry should |
439 | | * be inserted. The criteria used is to select the node whose MBR needs |
440 | | * the least enlargement to include the new entry. We resolve ties by |
441 | | * choosing the entry with the rectangle of smallest area. |
442 | | * (This is the ChooseSubtree part of Guttman's "ChooseLeaf" algorithm.) |
443 | | * |
444 | | * Returns the index of the best candidate or -1 of node is empty. |
445 | | **********************************************************************/ |
446 | | int TABMAPIndexBlock::ChooseSubEntryForInsert(GInt32 nXMin, GInt32 nYMin, |
447 | | GInt32 nXMax, GInt32 nYMax) |
448 | 136 | { |
449 | 136 | GInt32 nBestCandidate = -1; |
450 | | |
451 | 136 | double dOptimalAreaDiff = 0.0; |
452 | | |
453 | 136 | const double dNewEntryArea = MITAB_AREA(nXMin, nYMin, nXMax, nYMax); |
454 | | |
455 | 1.96k | for (GInt32 i = 0; i < m_numEntries; i++) |
456 | 1.82k | { |
457 | 1.82k | double dAreaDiff = 0.0; |
458 | 1.82k | const double dAreaBefore = |
459 | 1.82k | MITAB_AREA(m_asEntries[i].XMin, m_asEntries[i].YMin, |
460 | 1.82k | m_asEntries[i].XMax, m_asEntries[i].YMax); |
461 | | |
462 | | /* Does this entry fully contain the new entry's MBR ? |
463 | | */ |
464 | 1.82k | const GBool bIsContained = |
465 | 1.82k | nXMin >= m_asEntries[i].XMin && nYMin >= m_asEntries[i].YMin && |
466 | 1.82k | nXMax <= m_asEntries[i].XMax && nYMax <= m_asEntries[i].YMax; |
467 | | |
468 | 1.82k | if (bIsContained) |
469 | 1.43k | { |
470 | | /* If new entry is fully contained in this entry then |
471 | | * the area difference will be the difference between the area |
472 | | * of the entry to insert and the area of m_asEntries[i] |
473 | | * |
474 | | * The diff value is negative in this case. |
475 | | */ |
476 | 1.43k | dAreaDiff = dNewEntryArea - dAreaBefore; |
477 | 1.43k | } |
478 | 388 | else |
479 | 388 | { |
480 | | /* Need to calculate the expanded MBR to calculate the area |
481 | | * difference. |
482 | | */ |
483 | 388 | GInt32 nXMin2 = std::min(m_asEntries[i].XMin, nXMin); |
484 | 388 | GInt32 nYMin2 = std::min(m_asEntries[i].YMin, nYMin); |
485 | 388 | GInt32 nXMax2 = std::max(m_asEntries[i].XMax, nXMax); |
486 | 388 | GInt32 nYMax2 = std::max(m_asEntries[i].YMax, nYMax); |
487 | | |
488 | 388 | dAreaDiff = |
489 | 388 | MITAB_AREA(nXMin2, nYMin2, nXMax2, nYMax2) - dAreaBefore; |
490 | 388 | } |
491 | | |
492 | | /* Is this a better candidate? |
493 | | * Note, possible Optimization: In case of tie, we could to pick the |
494 | | * candidate with the smallest area |
495 | | */ |
496 | | |
497 | 1.82k | if (/* No best candidate yet */ |
498 | 1.82k | (nBestCandidate == -1) |
499 | | /* or current candidate is contained and best candidate is not |
500 | | contained */ |
501 | 1.82k | || (dAreaDiff < 0 && dOptimalAreaDiff >= 0) |
502 | | /* or if both are either contained or not contained then use the one |
503 | | * with the smallest area diff, which means maximum coverage in the |
504 | | * case of contained rects, or minimum area increase when not |
505 | | * contained |
506 | | */ |
507 | 1.82k | || (((dOptimalAreaDiff < 0 && dAreaDiff < 0) || |
508 | 1.64k | (dOptimalAreaDiff > 0 && dAreaDiff > 0)) && |
509 | 1.64k | std::abs(dAreaDiff) < std::abs(dOptimalAreaDiff))) |
510 | 191 | { |
511 | 191 | nBestCandidate = i; |
512 | 191 | dOptimalAreaDiff = dAreaDiff; |
513 | 191 | } |
514 | 1.82k | } |
515 | | |
516 | 136 | return nBestCandidate; |
517 | 136 | } |
518 | | |
519 | | /********************************************************************** |
520 | | * TABMAPIndexBlock::ChooseLeafForInsert() |
521 | | * |
522 | | * Recursively search the tree until we find the best leaf to |
523 | | * contain the specified object MBR. |
524 | | * |
525 | | * Returns the nBlockPtr of the selected leaf node entry (should be a |
526 | | * ref to a TABMAPObjectBlock) or -1 on error. |
527 | | * |
528 | | * After this call, m_poCurChild will be pointing at the selected child |
529 | | * node, for use by later calls to UpdateLeafEntry() |
530 | | **********************************************************************/ |
531 | | GInt32 TABMAPIndexBlock::ChooseLeafForInsert(GInt32 nXMin, GInt32 nYMin, |
532 | | GInt32 nXMax, GInt32 nYMax) |
533 | 0 | { |
534 | 0 | GBool bFound = FALSE; |
535 | |
|
536 | 0 | if (m_numEntries < 0) |
537 | 0 | return -1; |
538 | | |
539 | | /*----------------------------------------------------------------- |
540 | | * Look for the best candidate to contain the new entry |
541 | | *----------------------------------------------------------------*/ |
542 | | |
543 | | // Make sure blocks currently in memory are written to disk. |
544 | | // TODO: Could we avoid deleting m_poCurChild if it is already |
545 | | // the best candidate for insert? |
546 | 0 | if (m_poCurChild) |
547 | 0 | { |
548 | 0 | m_poCurChild->CommitToFile(); |
549 | 0 | m_poCurChild.reset(); |
550 | 0 | m_nCurChildIndex = -1; |
551 | 0 | } |
552 | |
|
553 | 0 | int nBestCandidate = ChooseSubEntryForInsert(nXMin, nYMin, nXMax, nYMax); |
554 | |
|
555 | 0 | CPLAssert(nBestCandidate != -1); |
556 | 0 | if (nBestCandidate == -1) |
557 | 0 | return -1; /* This should never happen! */ |
558 | | |
559 | | // Try to load corresponding child... if it fails then we are |
560 | | // likely in a leaf node, so we'll add the new entry in the current |
561 | | // node. |
562 | | |
563 | | // Prevent error message if referred block not committed yet. |
564 | 0 | CPLPushErrorHandler(CPLQuietErrorHandler); |
565 | |
|
566 | 0 | auto poBlock = std::unique_ptr<TABRawBinBlock>( |
567 | 0 | TABCreateMAPBlockFromFile(m_fp, m_asEntries[nBestCandidate].nBlockPtr, |
568 | 0 | m_nBlockSize, TRUE, TABReadWrite)); |
569 | 0 | if (poBlock != nullptr && poBlock->GetBlockClass() == TABMAP_INDEX_BLOCK) |
570 | 0 | { |
571 | 0 | m_poCurChild.reset( |
572 | 0 | cpl::down_cast<TABMAPIndexBlock *>(poBlock.release())); |
573 | 0 | m_nCurChildIndex = nBestCandidate; |
574 | 0 | m_poCurChild->SetParentRef(this); |
575 | 0 | m_poCurChild->SetMAPBlockManagerRef(m_poBlockManagerRef); |
576 | 0 | bFound = TRUE; |
577 | 0 | } |
578 | |
|
579 | 0 | CPLPopErrorHandler(); |
580 | 0 | CPLErrorReset(); |
581 | |
|
582 | 0 | if (bFound) |
583 | 0 | { |
584 | | /*------------------------------------------------------------- |
585 | | * Found a child leaf... pass the call to it. |
586 | | *------------------------------------------------------------*/ |
587 | 0 | return m_poCurChild->ChooseLeafForInsert(nXMin, nYMin, nXMax, nYMax); |
588 | 0 | } |
589 | | |
590 | | /*------------------------------------------------------------- |
591 | | * Found no child index node... we must be at the leaf level |
592 | | * (leaf points at map object data blocks) so we return a ref |
593 | | * to the TABMAPObjBlock for insertion |
594 | | *------------------------------------------------------------*/ |
595 | 0 | return m_asEntries[nBestCandidate].nBlockPtr; |
596 | 0 | } |
597 | | |
598 | | /********************************************************************** |
599 | | * TABMAPIndexBlock::GetCurLeafEntryMBR() |
600 | | * |
601 | | * Get the MBR for specified nBlockPtr in the leaf at the end of the |
602 | | * chain of m_poCurChild refs. |
603 | | * |
604 | | * This method requires that the chain of m_poCurChild refs already point |
605 | | * to a leaf that contains the specified nBlockPtr, it is usually called |
606 | | * right after ChooseLeafForInsert(). |
607 | | * |
608 | | * Returns 0 on success, -1 on error. |
609 | | **********************************************************************/ |
610 | | int TABMAPIndexBlock::GetCurLeafEntryMBR(GInt32 nBlockPtr, GInt32 &nXMin, |
611 | | GInt32 &nYMin, GInt32 &nXMax, |
612 | | GInt32 &nYMax) |
613 | 0 | { |
614 | 0 | if (m_poCurChild) |
615 | 0 | { |
616 | | /* Pass the call down to current child */ |
617 | 0 | return m_poCurChild->GetCurLeafEntryMBR(nBlockPtr, nXMin, nYMin, nXMax, |
618 | 0 | nYMax); |
619 | 0 | } |
620 | | |
621 | | /* We're at the leaf level, look for the entry */ |
622 | 0 | for (int i = 0; i < m_numEntries; i++) |
623 | 0 | { |
624 | 0 | if (m_asEntries[i].nBlockPtr == nBlockPtr) |
625 | 0 | { |
626 | | /* Found it. Return its MBR */ |
627 | 0 | nXMin = m_asEntries[i].XMin; |
628 | 0 | nYMin = m_asEntries[i].YMin; |
629 | 0 | nXMax = m_asEntries[i].XMax; |
630 | 0 | nYMax = m_asEntries[i].YMax; |
631 | |
|
632 | 0 | return 0; |
633 | 0 | } |
634 | 0 | } |
635 | | |
636 | | /* Not found! This should not happen if method is used properly. */ |
637 | 0 | CPLError(CE_Failure, CPLE_AssertionFailed, |
638 | 0 | "Entry to update not found in GetCurLeafEntryMBR()!"); |
639 | 0 | return -1; |
640 | 0 | } |
641 | | |
642 | | /********************************************************************** |
643 | | * TABMAPIndexBlock::UpdateLeafEntry() |
644 | | * |
645 | | * Update the MBR for specified nBlockPtr in the leaf at the end of the |
646 | | * chain of m_poCurChild refs and update MBR of parents if required. |
647 | | * |
648 | | * This method requires that the chain of m_poCurChild refs already point |
649 | | * to a leaf that contains the specified nBlockPtr, it is usually called |
650 | | * right after ChooseLeafForInsert(). |
651 | | * |
652 | | * Returns 0 on success, -1 on error. |
653 | | **********************************************************************/ |
654 | | int TABMAPIndexBlock::UpdateLeafEntry(GInt32 nBlockPtr, GInt32 nXMin, |
655 | | GInt32 nYMin, GInt32 nXMax, GInt32 nYMax) |
656 | 0 | { |
657 | 0 | if (m_poCurChild) |
658 | 0 | { |
659 | | /* Pass the call down to current child */ |
660 | 0 | return m_poCurChild->UpdateLeafEntry(nBlockPtr, nXMin, nYMin, nXMax, |
661 | 0 | nYMax); |
662 | 0 | } |
663 | | |
664 | | /* We're at the leaf level, look for the entry to update */ |
665 | 0 | for (int i = 0; i < m_numEntries; i++) |
666 | 0 | { |
667 | 0 | if (m_asEntries[i].nBlockPtr == nBlockPtr) |
668 | 0 | { |
669 | | /* Found it. */ |
670 | 0 | TABMAPIndexEntry *psEntry = &m_asEntries[i]; |
671 | |
|
672 | 0 | if (psEntry->XMin != nXMin || psEntry->YMin != nYMin || |
673 | 0 | psEntry->XMax != nXMax || psEntry->YMax != nYMax) |
674 | 0 | { |
675 | | /* MBR changed. Update MBR of entry */ |
676 | 0 | psEntry->XMin = nXMin; |
677 | 0 | psEntry->YMin = nYMin; |
678 | 0 | psEntry->XMax = nXMax; |
679 | 0 | psEntry->YMax = nYMax; |
680 | |
|
681 | 0 | m_bModified = TRUE; |
682 | | |
683 | | /* Update MBR of this node and all parents */ |
684 | 0 | RecomputeMBR(); |
685 | 0 | } |
686 | |
|
687 | 0 | return 0; |
688 | 0 | } |
689 | 0 | } |
690 | | |
691 | | /* Not found! This should not happen if method is used properly. */ |
692 | 0 | CPLError(CE_Failure, CPLE_AssertionFailed, |
693 | 0 | "Entry to update not found in UpdateLeafEntry()!"); |
694 | 0 | return -1; |
695 | 0 | } |
696 | | |
697 | | /********************************************************************** |
698 | | * TABMAPIndexBlock::AddEntry() |
699 | | * |
700 | | * Recursively search the tree until we encounter the best leaf to |
701 | | * contain the specified object MBR and add the new entry to it. |
702 | | * |
703 | | * In the even that the selected leaf node would be full, then it will be |
704 | | * split and this split can propagate up to its parent, etc. |
705 | | * |
706 | | * If bAddInThisNodeOnly=TRUE, then the entry is added only locally and |
707 | | * we do not try to update the child node. This is used when the parent |
708 | | * of a node that is being split has to be updated. |
709 | | * |
710 | | * Returns 0 on success, -1 on error. |
711 | | **********************************************************************/ |
712 | | int TABMAPIndexBlock::AddEntry(GInt32 nXMin, GInt32 nYMin, GInt32 nXMax, |
713 | | GInt32 nYMax, GInt32 nBlockPtr, |
714 | | GBool bAddInThisNodeOnly /*=FALSE*/) |
715 | 184 | { |
716 | 184 | GBool bFound = FALSE; |
717 | | |
718 | 184 | if (m_eAccess != TABWrite && m_eAccess != TABReadWrite) |
719 | 0 | { |
720 | 0 | CPLError( |
721 | 0 | CE_Failure, CPLE_AssertionFailed, |
722 | 0 | "Failed adding index entry: File not opened for write access."); |
723 | 0 | return -1; |
724 | 0 | } |
725 | | |
726 | | /*----------------------------------------------------------------- |
727 | | * Look for the best candidate to contain the new entry |
728 | | *----------------------------------------------------------------*/ |
729 | | |
730 | | /*----------------------------------------------------------------- |
731 | | * If bAddInThisNodeOnly=TRUE then we add the entry only locally |
732 | | * and do not need to look for the proper leaf to insert it. |
733 | | *----------------------------------------------------------------*/ |
734 | 184 | if (bAddInThisNodeOnly) |
735 | 39 | bFound = TRUE; |
736 | | |
737 | 184 | if (!bFound && m_numEntries > 0) |
738 | 136 | { |
739 | | // Make sure blocks currently in memory are written to disk. |
740 | 136 | if (m_poCurChild) |
741 | 41 | { |
742 | 41 | m_poCurChild->CommitToFile(); |
743 | 41 | m_poCurChild.reset(); |
744 | 41 | m_nCurChildIndex = -1; |
745 | 41 | } |
746 | | |
747 | 136 | int nBestCandidate = |
748 | 136 | ChooseSubEntryForInsert(nXMin, nYMin, nXMax, nYMax); |
749 | | |
750 | 136 | CPLAssert(nBestCandidate != -1); |
751 | | |
752 | 136 | if (nBestCandidate != -1) |
753 | 136 | { |
754 | | // Try to load corresponding child... if it fails then we are |
755 | | // likely in a leaf node, so we'll add the new entry in the current |
756 | | // node. |
757 | | |
758 | | // Prevent error message if referred block not committed yet. |
759 | 136 | CPLPushErrorHandler(CPLQuietErrorHandler); |
760 | | |
761 | 136 | auto poBlock = |
762 | 136 | std::unique_ptr<TABRawBinBlock>(TABCreateMAPBlockFromFile( |
763 | 136 | m_fp, m_asEntries[nBestCandidate].nBlockPtr, m_nBlockSize, |
764 | 136 | TRUE, TABReadWrite)); |
765 | 136 | if (poBlock != nullptr && |
766 | 136 | poBlock->GetBlockClass() == TABMAP_INDEX_BLOCK) |
767 | 58 | { |
768 | 58 | m_poCurChild.reset( |
769 | 58 | cpl::down_cast<TABMAPIndexBlock *>(poBlock.release())); |
770 | 58 | m_nCurChildIndex = nBestCandidate; |
771 | 58 | m_poCurChild->SetParentRef(this); |
772 | 58 | m_poCurChild->SetMAPBlockManagerRef(m_poBlockManagerRef); |
773 | 58 | bFound = TRUE; |
774 | 58 | } |
775 | | |
776 | 136 | CPLPopErrorHandler(); |
777 | 136 | CPLErrorReset(); |
778 | 136 | } |
779 | 136 | } |
780 | | |
781 | 184 | if (bFound && !bAddInThisNodeOnly) |
782 | 58 | { |
783 | | /*------------------------------------------------------------- |
784 | | * Found a child leaf... pass the call to it. |
785 | | *------------------------------------------------------------*/ |
786 | 58 | if (m_poCurChild->AddEntry(nXMin, nYMin, nXMax, nYMax, nBlockPtr) != 0) |
787 | 0 | return -1; |
788 | 58 | } |
789 | 126 | else |
790 | 126 | { |
791 | | /*------------------------------------------------------------- |
792 | | * Found no child to store new object... we're likely at the leaf |
793 | | * level so we'll store new object in current node |
794 | | *------------------------------------------------------------*/ |
795 | | |
796 | | /*------------------------------------------------------------- |
797 | | * First thing to do is make sure that there is room for a new |
798 | | * entry in this node, and to split it if necessary. |
799 | | *------------------------------------------------------------*/ |
800 | 126 | if (GetNumFreeEntries() < 1) |
801 | 37 | { |
802 | 37 | if (m_poParentRef == nullptr) |
803 | 2 | { |
804 | | /*----------------------------------------------------- |
805 | | * Splitting the root node adds one level to the tree, so |
806 | | * after splitting we just redirect the call to the new |
807 | | * child that's just been created. |
808 | | *----------------------------------------------------*/ |
809 | 2 | if (SplitRootNode(nXMin, nYMin, nXMax, nYMax) != 0) |
810 | 0 | return -1; // Error happened and has already been reported |
811 | | |
812 | 2 | CPLAssert(m_poCurChild); |
813 | 2 | return m_poCurChild->AddEntry(nXMin, nYMin, nXMax, nYMax, |
814 | 2 | nBlockPtr, TRUE); |
815 | 2 | } |
816 | 35 | else |
817 | 35 | { |
818 | | /*----------------------------------------------------- |
819 | | * Splitting a regular node |
820 | | *----------------------------------------------------*/ |
821 | 35 | if (SplitNode(nXMin, nYMin, nXMax, nYMax) != 0) |
822 | 0 | return -1; |
823 | 35 | } |
824 | 37 | } |
825 | | |
826 | 124 | if (InsertEntry(nXMin, nYMin, nXMax, nYMax, nBlockPtr) != 0) |
827 | 0 | return -1; |
828 | 124 | } |
829 | | |
830 | | /*----------------------------------------------------------------- |
831 | | * Update current node MBR and the reference to it in our parent. |
832 | | *----------------------------------------------------------------*/ |
833 | 182 | RecomputeMBR(); |
834 | | |
835 | 182 | return 0; |
836 | 184 | } |
837 | | |
838 | | /********************************************************************** |
839 | | * TABMAPIndexBlock::ComputeAreaDiff() |
840 | | * |
841 | | * (static method, also used by the TABMAPObjBlock class) |
842 | | * |
843 | | * Compute the area difference between two MBRs. Used in the SplitNode |
844 | | * algorithm to decide to which of the two nodes an entry should be added. |
845 | | * |
846 | | * The returned AreaDiff value is positive if NodeMBR has to be enlarged |
847 | | * and negative if new Entry is fully contained in the NodeMBR. |
848 | | **********************************************************************/ |
849 | | double TABMAPIndexBlock::ComputeAreaDiff(GInt32 nNodeXMin, GInt32 nNodeYMin, |
850 | | GInt32 nNodeXMax, GInt32 nNodeYMax, |
851 | | GInt32 nEntryXMin, GInt32 nEntryYMin, |
852 | | GInt32 nEntryXMax, GInt32 nEntryYMax) |
853 | 1.77k | { |
854 | 1.77k | double dAreaDiff = 0.0; |
855 | | |
856 | 1.77k | const double dNodeAreaBefore = |
857 | 1.77k | MITAB_AREA(nNodeXMin, nNodeYMin, nNodeXMax, nNodeYMax); |
858 | | |
859 | | // Does the node fully contain the new entry's MBR? |
860 | 1.77k | const GBool bIsContained = |
861 | 1.77k | nEntryXMin >= nNodeXMin && nEntryYMin >= nNodeYMin && |
862 | 1.77k | nEntryXMax <= nNodeXMax && nEntryYMax <= nNodeYMax; |
863 | | |
864 | 1.77k | if (bIsContained) |
865 | 1.60k | { |
866 | | /* If new entry is fully contained in this entry then |
867 | | * the area difference will be the difference between the area |
868 | | * of the entry to insert and the area of the node |
869 | | */ |
870 | 1.60k | dAreaDiff = MITAB_AREA(nEntryXMin, nEntryYMin, nEntryXMax, nEntryYMax) - |
871 | 1.60k | dNodeAreaBefore; |
872 | 1.60k | } |
873 | 172 | else |
874 | 172 | { |
875 | | /* Need to calculate the expanded MBR to calculate the area |
876 | | * difference. |
877 | | */ |
878 | 172 | nNodeXMin = std::min(nNodeXMin, nEntryXMin); |
879 | 172 | nNodeYMin = std::min(nNodeYMin, nEntryYMin); |
880 | 172 | nNodeXMax = std::max(nNodeXMax, nEntryXMax); |
881 | 172 | nNodeYMax = std::max(nNodeYMax, nEntryYMax); |
882 | | |
883 | 172 | dAreaDiff = MITAB_AREA(nNodeXMin, nNodeYMin, nNodeXMax, nNodeYMax) - |
884 | 172 | dNodeAreaBefore; |
885 | 172 | } |
886 | | |
887 | 1.77k | return dAreaDiff; |
888 | 1.77k | } |
889 | | |
890 | | /********************************************************************** |
891 | | * TABMAPIndexBlock::PickSeedsForSplit() |
892 | | * |
893 | | * (static method, also used by the TABMAPObjBlock class) |
894 | | * |
895 | | * Pick two seeds to use to start splitting this node. |
896 | | * |
897 | | * Guttman's LinearPickSeed: |
898 | | * - Along each dimension find the entry whose rectangle has the |
899 | | * highest low side, and the one with the lowest high side |
900 | | * - Calculate the separation for each pair |
901 | | * - Normalize the separation by dividing by the extents of the |
902 | | * corresponding dimension |
903 | | * - Choose the pair with the greatest normalized separation along |
904 | | * any dimension |
905 | | **********************************************************************/ |
906 | | int TABMAPIndexBlock::PickSeedsForSplit( |
907 | | TABMAPIndexEntry *pasEntries, int numEntries, int nSrcCurChildIndex, |
908 | | GInt32 nNewEntryXMin, GInt32 nNewEntryYMin, GInt32 nNewEntryXMax, |
909 | | GInt32 nNewEntryYMax, int &nSeed1, int &nSeed2) |
910 | 37 | { |
911 | 37 | GInt32 nSrcMinX = 0; |
912 | 37 | GInt32 nSrcMinY = 0; |
913 | 37 | GInt32 nSrcMaxX = 0; |
914 | 37 | GInt32 nSrcMaxY = 0; |
915 | 37 | int nLowestMaxX = -1; |
916 | 37 | int nHighestMinX = -1; |
917 | 37 | int nLowestMaxY = -1; |
918 | 37 | int nHighestMinY = -1; |
919 | 37 | GInt32 nLowestMaxXId = -1; |
920 | 37 | GInt32 nHighestMinXId = -1; |
921 | 37 | GInt32 nLowestMaxYId = -1; |
922 | 37 | GInt32 nHighestMinYId = -1; |
923 | | |
924 | 37 | nSeed1 = -1; |
925 | 37 | nSeed2 = -1; |
926 | | |
927 | | // Along each dimension find the entry whose rectangle has the |
928 | | // highest low side, and the one with the lowest high side |
929 | 962 | for (int iEntry = 0; iEntry < numEntries; iEntry++) |
930 | 925 | { |
931 | 925 | if (nLowestMaxXId == -1 || pasEntries[iEntry].XMax < nLowestMaxX) |
932 | 71 | { |
933 | 71 | nLowestMaxX = pasEntries[iEntry].XMax; |
934 | 71 | nLowestMaxXId = iEntry; |
935 | 71 | } |
936 | | |
937 | 925 | if (nHighestMinXId == -1 || pasEntries[iEntry].XMin > nHighestMinX) |
938 | 37 | { |
939 | 37 | nHighestMinX = pasEntries[iEntry].XMin; |
940 | 37 | nHighestMinXId = iEntry; |
941 | 37 | } |
942 | | |
943 | 925 | if (nLowestMaxYId == -1 || pasEntries[iEntry].YMax < nLowestMaxY) |
944 | 71 | { |
945 | 71 | nLowestMaxY = pasEntries[iEntry].YMax; |
946 | 71 | nLowestMaxYId = iEntry; |
947 | 71 | } |
948 | | |
949 | 925 | if (nHighestMinYId == -1 || pasEntries[iEntry].YMin > nHighestMinY) |
950 | 38 | { |
951 | 38 | nHighestMinY = pasEntries[iEntry].YMin; |
952 | 38 | nHighestMinYId = iEntry; |
953 | 38 | } |
954 | | |
955 | | // Also keep track of MBR of all entries |
956 | 925 | if (iEntry == 0) |
957 | 37 | { |
958 | 37 | nSrcMinX = pasEntries[iEntry].XMin; |
959 | 37 | nSrcMinY = pasEntries[iEntry].YMin; |
960 | 37 | nSrcMaxX = pasEntries[iEntry].XMax; |
961 | 37 | nSrcMaxY = pasEntries[iEntry].YMax; |
962 | 37 | } |
963 | 888 | else |
964 | 888 | { |
965 | 888 | nSrcMinX = std::min(nSrcMinX, pasEntries[iEntry].XMin); |
966 | 888 | nSrcMinY = std::min(nSrcMinY, pasEntries[iEntry].YMin); |
967 | 888 | nSrcMaxX = std::max(nSrcMaxX, pasEntries[iEntry].XMax); |
968 | 888 | nSrcMaxY = std::max(nSrcMaxY, pasEntries[iEntry].YMax); |
969 | 888 | } |
970 | 925 | } |
971 | | |
972 | 37 | const double dfSrcWidth = |
973 | 37 | std::abs(static_cast<double>(nSrcMaxX) - nSrcMinX); |
974 | 37 | const double dfSrcHeight = |
975 | 37 | std::abs(static_cast<double>(nSrcMaxY) - nSrcMinY); |
976 | | |
977 | | // Calculate the separation for each pair (note that it may be negative |
978 | | // in case of overlap) |
979 | | // Normalize the separation by dividing by the extents of the |
980 | | // corresponding dimension |
981 | 37 | const double dX = |
982 | 37 | dfSrcWidth == 0.0 |
983 | 37 | ? 0.0 |
984 | 37 | : (static_cast<double>(nHighestMinX) - nLowestMaxX) / dfSrcWidth; |
985 | 37 | const double dY = |
986 | 37 | dfSrcHeight == 0.0 |
987 | 37 | ? 0.0 |
988 | 37 | : (static_cast<double>(nHighestMinY) - nLowestMaxY) / dfSrcHeight; |
989 | | |
990 | | // Choose the pair with the greatest normalized separation along |
991 | | // any dimension |
992 | 37 | if (dX > dY) |
993 | 0 | { |
994 | 0 | nSeed1 = nHighestMinXId; |
995 | 0 | nSeed2 = nLowestMaxXId; |
996 | 0 | } |
997 | 37 | else |
998 | 37 | { |
999 | 37 | nSeed1 = nHighestMinYId; |
1000 | 37 | nSeed2 = nLowestMaxYId; |
1001 | 37 | } |
1002 | | |
1003 | | // If nSeed1==nSeed2 then just pick any two (giving pref to current child) |
1004 | 37 | if (nSeed1 == nSeed2) |
1005 | 2 | { |
1006 | 2 | if (nSeed1 != nSrcCurChildIndex && nSrcCurChildIndex != -1) |
1007 | 0 | nSeed1 = nSrcCurChildIndex; |
1008 | 2 | else if (nSeed1 != 0) |
1009 | 0 | nSeed1 = 0; |
1010 | 2 | else |
1011 | 2 | nSeed1 = 1; |
1012 | 2 | } |
1013 | | |
1014 | | // Decide which of the two seeds best matches the new entry. That seed and |
1015 | | // the new entry will stay in current node (new entry will be added by the |
1016 | | // caller later). The other seed will go in the 2nd node |
1017 | 37 | const double dAreaDiff1 = ComputeAreaDiff( |
1018 | 37 | pasEntries[nSeed1].XMin, pasEntries[nSeed1].YMin, |
1019 | 37 | pasEntries[nSeed1].XMax, pasEntries[nSeed1].YMax, nNewEntryXMin, |
1020 | 37 | nNewEntryYMin, nNewEntryXMax, nNewEntryYMax); |
1021 | | |
1022 | 37 | const double dAreaDiff2 = ComputeAreaDiff( |
1023 | 37 | pasEntries[nSeed2].XMin, pasEntries[nSeed2].YMin, |
1024 | 37 | pasEntries[nSeed2].XMax, pasEntries[nSeed2].YMax, nNewEntryXMin, |
1025 | 37 | nNewEntryYMin, nNewEntryXMax, nNewEntryYMax); |
1026 | | |
1027 | | /* Note that we want to keep this node's current child in here. |
1028 | | * Since splitting happens only during an addentry() operation and |
1029 | | * then both the current child and the New Entry should fit in the same |
1030 | | * area. |
1031 | | */ |
1032 | 37 | if (nSeed1 != nSrcCurChildIndex && |
1033 | 37 | (dAreaDiff1 > dAreaDiff2 || nSeed2 == nSrcCurChildIndex)) |
1034 | 3 | { |
1035 | | // Seed2 stays in this node, Seed1 moves to new node |
1036 | | // ... swap Seed1 and Seed2 indices |
1037 | 3 | int nTmp = nSeed1; |
1038 | 3 | nSeed1 = nSeed2; |
1039 | 3 | nSeed2 = nTmp; |
1040 | 3 | } |
1041 | | |
1042 | 37 | return 0; |
1043 | 37 | } |
1044 | | |
1045 | | /********************************************************************** |
1046 | | * TABMAPIndexBlock::SplitNode() |
1047 | | * |
1048 | | * Split current Node, update the references in the parent node, etc. |
1049 | | * Note that Root Nodes cannot be split using this method... SplitRootNode() |
1050 | | * should be used instead. |
1051 | | * |
1052 | | * nNewEntry* are the coord. of the new entry that |
1053 | | * will be added after the split. The split is done so that the current |
1054 | | * node will be the one in which the new object should be stored. |
1055 | | * |
1056 | | * Returns 0 on success, -1 on error. |
1057 | | **********************************************************************/ |
1058 | | int TABMAPIndexBlock::SplitNode(GInt32 nNewEntryXMin, GInt32 nNewEntryYMin, |
1059 | | GInt32 nNewEntryXMax, GInt32 nNewEntryYMax) |
1060 | 37 | { |
1061 | 37 | CPLAssert(m_poBlockManagerRef); |
1062 | | |
1063 | | /*----------------------------------------------------------------- |
1064 | | * Create a 2nd node |
1065 | | *----------------------------------------------------------------*/ |
1066 | 37 | TABMAPIndexBlock *poNewNode = new TABMAPIndexBlock(m_eAccess); |
1067 | 37 | if (poNewNode->InitNewBlock(m_fp, m_nBlockSize, |
1068 | 37 | m_poBlockManagerRef->AllocNewBlock("INDEX")) != |
1069 | 37 | 0) |
1070 | 0 | { |
1071 | 0 | return -1; |
1072 | 0 | } |
1073 | 37 | poNewNode->SetMAPBlockManagerRef(m_poBlockManagerRef); |
1074 | | |
1075 | | /*----------------------------------------------------------------- |
1076 | | * Make a temporary copy of the entries in current node |
1077 | | *----------------------------------------------------------------*/ |
1078 | 37 | int nSrcEntries = m_numEntries; |
1079 | 37 | TABMAPIndexEntry *pasSrcEntries = static_cast<TABMAPIndexEntry *>( |
1080 | 37 | CPLMalloc(m_numEntries * sizeof(TABMAPIndexEntry))); |
1081 | 37 | memcpy(pasSrcEntries, &m_asEntries, |
1082 | 37 | m_numEntries * sizeof(TABMAPIndexEntry)); |
1083 | | |
1084 | 37 | int nSrcCurChildIndex = m_nCurChildIndex; |
1085 | | |
1086 | | /*----------------------------------------------------------------- |
1087 | | * Pick Seeds for each node |
1088 | | *----------------------------------------------------------------*/ |
1089 | 37 | int nSeed1, nSeed2; |
1090 | 37 | PickSeedsForSplit(pasSrcEntries, nSrcEntries, nSrcCurChildIndex, |
1091 | 37 | nNewEntryXMin, nNewEntryYMin, nNewEntryXMax, |
1092 | 37 | nNewEntryYMax, nSeed1, nSeed2); |
1093 | | |
1094 | | /*----------------------------------------------------------------- |
1095 | | * Reset number of entries in this node and start moving new entries |
1096 | | *----------------------------------------------------------------*/ |
1097 | 37 | m_numEntries = 0; |
1098 | | |
1099 | | // Insert nSeed1 in this node |
1100 | 37 | InsertEntry(pasSrcEntries[nSeed1].XMin, pasSrcEntries[nSeed1].YMin, |
1101 | 37 | pasSrcEntries[nSeed1].XMax, pasSrcEntries[nSeed1].YMax, |
1102 | 37 | pasSrcEntries[nSeed1].nBlockPtr); |
1103 | | |
1104 | | // Move nSeed2 to 2nd node |
1105 | 37 | poNewNode->InsertEntry( |
1106 | 37 | pasSrcEntries[nSeed2].XMin, pasSrcEntries[nSeed2].YMin, |
1107 | 37 | pasSrcEntries[nSeed2].XMax, pasSrcEntries[nSeed2].YMax, |
1108 | 37 | pasSrcEntries[nSeed2].nBlockPtr); |
1109 | | |
1110 | | // Update cur child index if necessary |
1111 | 37 | if (nSeed1 == nSrcCurChildIndex) |
1112 | 1 | m_nCurChildIndex = m_numEntries - 1; |
1113 | | |
1114 | | /*----------------------------------------------------------------- |
1115 | | * Go through the rest of the entries and assign them to one |
1116 | | * of the 2 nodes. |
1117 | | * |
1118 | | * Criteria is minimal area difference. |
1119 | | * Resolve ties by adding the entry to the node with smaller total |
1120 | | * area, then to the one with fewer entries, then to either. |
1121 | | *----------------------------------------------------------------*/ |
1122 | 962 | for (int iEntry = 0; iEntry < nSrcEntries; iEntry++) |
1123 | 925 | { |
1124 | 925 | if (iEntry == nSeed1 || iEntry == nSeed2) |
1125 | 74 | continue; |
1126 | | |
1127 | | // If one of the two nodes is almost full then all remaining |
1128 | | // entries should go to the other node |
1129 | | // The entry corresponding to the current child also automatically |
1130 | | // stays in this node. |
1131 | 851 | if (iEntry == nSrcCurChildIndex) |
1132 | 0 | { |
1133 | 0 | InsertEntry(pasSrcEntries[iEntry].XMin, pasSrcEntries[iEntry].YMin, |
1134 | 0 | pasSrcEntries[iEntry].XMax, pasSrcEntries[iEntry].YMax, |
1135 | 0 | pasSrcEntries[iEntry].nBlockPtr); |
1136 | | |
1137 | | // Update current child index |
1138 | 0 | m_nCurChildIndex = m_numEntries - 1; |
1139 | |
|
1140 | 0 | continue; |
1141 | 0 | } |
1142 | 851 | else if (m_numEntries >= GetMaxEntries() - 1) |
1143 | 0 | { |
1144 | 0 | poNewNode->InsertEntry( |
1145 | 0 | pasSrcEntries[iEntry].XMin, pasSrcEntries[iEntry].YMin, |
1146 | 0 | pasSrcEntries[iEntry].XMax, pasSrcEntries[iEntry].YMax, |
1147 | 0 | pasSrcEntries[iEntry].nBlockPtr); |
1148 | 0 | continue; |
1149 | 0 | } |
1150 | 851 | else if (poNewNode->GetNumEntries() >= GetMaxEntries() - 1) |
1151 | 0 | { |
1152 | 0 | InsertEntry(pasSrcEntries[iEntry].XMin, pasSrcEntries[iEntry].YMin, |
1153 | 0 | pasSrcEntries[iEntry].XMax, pasSrcEntries[iEntry].YMax, |
1154 | 0 | pasSrcEntries[iEntry].nBlockPtr); |
1155 | 0 | continue; |
1156 | 0 | } |
1157 | | |
1158 | | // Decide which of the two nodes to put this entry in |
1159 | 851 | RecomputeMBR(); |
1160 | 851 | const double dAreaDiff1 = ComputeAreaDiff( |
1161 | 851 | m_nMinX, m_nMinY, m_nMaxX, m_nMaxY, pasSrcEntries[iEntry].XMin, |
1162 | 851 | pasSrcEntries[iEntry].YMin, pasSrcEntries[iEntry].XMax, |
1163 | 851 | pasSrcEntries[iEntry].YMax); |
1164 | | |
1165 | 851 | GInt32 nXMin2 = 0; |
1166 | 851 | GInt32 nYMin2 = 0; |
1167 | 851 | GInt32 nXMax2 = 0; |
1168 | 851 | GInt32 nYMax2 = 0; |
1169 | 851 | poNewNode->RecomputeMBR(); |
1170 | 851 | poNewNode->GetMBR(nXMin2, nYMin2, nXMax2, nYMax2); |
1171 | 851 | const double dAreaDiff2 = ComputeAreaDiff( |
1172 | 851 | nXMin2, nYMin2, nXMax2, nYMax2, pasSrcEntries[iEntry].XMin, |
1173 | 851 | pasSrcEntries[iEntry].YMin, pasSrcEntries[iEntry].XMax, |
1174 | 851 | pasSrcEntries[iEntry].YMax); |
1175 | 851 | if (dAreaDiff1 < dAreaDiff2) |
1176 | 782 | { |
1177 | | // This entry stays in this node. |
1178 | 782 | InsertEntry(pasSrcEntries[iEntry].XMin, pasSrcEntries[iEntry].YMin, |
1179 | 782 | pasSrcEntries[iEntry].XMax, pasSrcEntries[iEntry].YMax, |
1180 | 782 | pasSrcEntries[iEntry].nBlockPtr); |
1181 | 782 | } |
1182 | 69 | else |
1183 | 69 | { |
1184 | | // This entry goes to new node |
1185 | 69 | poNewNode->InsertEntry( |
1186 | 69 | pasSrcEntries[iEntry].XMin, pasSrcEntries[iEntry].YMin, |
1187 | 69 | pasSrcEntries[iEntry].XMax, pasSrcEntries[iEntry].YMax, |
1188 | 69 | pasSrcEntries[iEntry].nBlockPtr); |
1189 | 69 | } |
1190 | 851 | } |
1191 | | |
1192 | | /*----------------------------------------------------------------- |
1193 | | * Recompute MBR and update current node info in parent |
1194 | | *----------------------------------------------------------------*/ |
1195 | 37 | RecomputeMBR(); |
1196 | 37 | poNewNode->RecomputeMBR(); |
1197 | | |
1198 | | /*----------------------------------------------------------------- |
1199 | | * Add second node info to parent and then flush it to disk. |
1200 | | * This may trigger splitting of parent |
1201 | | *----------------------------------------------------------------*/ |
1202 | 37 | CPLAssert(m_poParentRef); |
1203 | 37 | int nMinX, nMinY, nMaxX, nMaxY; |
1204 | 37 | poNewNode->GetMBR(nMinX, nMinY, nMaxX, nMaxY); |
1205 | 37 | m_poParentRef->AddEntry(nMinX, nMinY, nMaxX, nMaxY, |
1206 | 37 | poNewNode->GetNodeBlockPtr(), TRUE); |
1207 | 37 | poNewNode->CommitToFile(); |
1208 | 37 | delete poNewNode; |
1209 | | |
1210 | 37 | CPLFree(pasSrcEntries); |
1211 | | |
1212 | 37 | return 0; |
1213 | 37 | } |
1214 | | |
1215 | | /********************************************************************** |
1216 | | * TABMAPIndexBlock::SplitRootNode() |
1217 | | * |
1218 | | * (private method) |
1219 | | * |
1220 | | * Split a Root Node. |
1221 | | * First, a level of nodes must be added to the tree, then the contents |
1222 | | * of what used to be the root node is moved 1 level down and then that |
1223 | | * node is split like a regular node. |
1224 | | * |
1225 | | * Returns 0 on success, -1 on error |
1226 | | **********************************************************************/ |
1227 | | int TABMAPIndexBlock::SplitRootNode(GInt32 nNewEntryXMin, GInt32 nNewEntryYMin, |
1228 | | GInt32 nNewEntryXMax, GInt32 nNewEntryYMax) |
1229 | 2 | { |
1230 | 2 | CPLAssert(m_poBlockManagerRef); |
1231 | 2 | CPLAssert(m_poParentRef == nullptr); |
1232 | | |
1233 | | /*----------------------------------------------------------------- |
1234 | | * Since a root note cannot be split, we add a level of nodes |
1235 | | * under it and we'll do the split at that level. |
1236 | | *----------------------------------------------------------------*/ |
1237 | 2 | auto poNewNode = std::make_unique<TABMAPIndexBlock>(m_eAccess); |
1238 | | |
1239 | 2 | if (poNewNode->InitNewBlock(m_fp, m_nBlockSize, |
1240 | 2 | m_poBlockManagerRef->AllocNewBlock("INDEX")) != |
1241 | 2 | 0) |
1242 | 0 | { |
1243 | 0 | return -1; |
1244 | 0 | } |
1245 | 2 | poNewNode->SetMAPBlockManagerRef(m_poBlockManagerRef); |
1246 | | |
1247 | | // Move all entries to the new child |
1248 | 2 | int nSrcEntries = m_numEntries; |
1249 | 2 | m_numEntries = 0; |
1250 | 52 | for (int iEntry = 0; iEntry < nSrcEntries; iEntry++) |
1251 | 50 | { |
1252 | 50 | poNewNode->InsertEntry( |
1253 | 50 | m_asEntries[iEntry].XMin, m_asEntries[iEntry].YMin, |
1254 | 50 | m_asEntries[iEntry].XMax, m_asEntries[iEntry].YMax, |
1255 | 50 | m_asEntries[iEntry].nBlockPtr); |
1256 | 50 | } |
1257 | | |
1258 | | /*----------------------------------------------------------------- |
1259 | | * Transfer current child object to new node. |
1260 | | *----------------------------------------------------------------*/ |
1261 | 2 | if (m_poCurChild) |
1262 | 1 | { |
1263 | 1 | poNewNode->SetCurChild(std::move(m_poCurChild), m_nCurChildIndex); |
1264 | 1 | m_nCurChildIndex = -1; |
1265 | 1 | } |
1266 | | |
1267 | | /*----------------------------------------------------------------- |
1268 | | * Place info about new child in current node. |
1269 | | *----------------------------------------------------------------*/ |
1270 | 2 | poNewNode->RecomputeMBR(); |
1271 | 2 | int nMinX, nMinY, nMaxX, nMaxY; |
1272 | 2 | poNewNode->GetMBR(nMinX, nMinY, nMaxX, nMaxY); |
1273 | 2 | InsertEntry(nMinX, nMinY, nMaxX, nMaxY, poNewNode->GetNodeBlockPtr()); |
1274 | | |
1275 | | /*----------------------------------------------------------------- |
1276 | | * Keep a reference to the new child |
1277 | | *----------------------------------------------------------------*/ |
1278 | 2 | poNewNode->SetParentRef(this); |
1279 | 2 | m_poCurChild = std::move(poNewNode); |
1280 | 2 | m_nCurChildIndex = m_numEntries - 1; |
1281 | | |
1282 | | /*----------------------------------------------------------------- |
1283 | | * And finally force the child to split itself |
1284 | | *----------------------------------------------------------------*/ |
1285 | 2 | return m_poCurChild->SplitNode(nNewEntryXMin, nNewEntryYMin, nNewEntryXMax, |
1286 | 2 | nNewEntryYMax); |
1287 | 2 | } |
1288 | | |
1289 | | /********************************************************************** |
1290 | | * TABMAPIndexBlock::RecomputeMBR() |
1291 | | * |
1292 | | * Recompute current block MBR, and update info in parent. |
1293 | | **********************************************************************/ |
1294 | | void TABMAPIndexBlock::RecomputeMBR() |
1295 | 1.96k | { |
1296 | 1.96k | GInt32 nMinX, nMinY, nMaxX, nMaxY; |
1297 | | |
1298 | 1.96k | nMinX = 1000000000; |
1299 | 1.96k | nMinY = 1000000000; |
1300 | 1.96k | nMaxX = -1000000000; |
1301 | 1.96k | nMaxY = -1000000000; |
1302 | | |
1303 | 16.2k | for (int i = 0; i < m_numEntries; i++) |
1304 | 14.2k | { |
1305 | 14.2k | if (m_asEntries[i].XMin < nMinX) |
1306 | 1.97k | nMinX = m_asEntries[i].XMin; |
1307 | 14.2k | if (m_asEntries[i].XMax > nMaxX) |
1308 | 2.03k | nMaxX = m_asEntries[i].XMax; |
1309 | | |
1310 | 14.2k | if (m_asEntries[i].YMin < nMinY) |
1311 | 2.90k | nMinY = m_asEntries[i].YMin; |
1312 | 14.2k | if (m_asEntries[i].YMax > nMaxY) |
1313 | 2.03k | nMaxY = m_asEntries[i].YMax; |
1314 | 14.2k | } |
1315 | | |
1316 | 1.96k | if (m_nMinX != nMinX || m_nMinY != nMinY || m_nMaxX != nMaxX || |
1317 | 1.96k | m_nMaxY != nMaxY) |
1318 | 151 | { |
1319 | 151 | m_nMinX = nMinX; |
1320 | 151 | m_nMinY = nMinY; |
1321 | 151 | m_nMaxX = nMaxX; |
1322 | 151 | m_nMaxY = nMaxY; |
1323 | | |
1324 | 151 | m_bModified = TRUE; |
1325 | | |
1326 | 151 | if (m_poParentRef) |
1327 | 96 | m_poParentRef->UpdateCurChildMBR(m_nMinX, m_nMinY, m_nMaxX, m_nMaxY, |
1328 | 96 | GetNodeBlockPtr()); |
1329 | 151 | } |
1330 | 1.96k | } |
1331 | | |
1332 | | /********************************************************************** |
1333 | | * TABMAPIndexBlock::UpdateCurChildMBR() |
1334 | | * |
1335 | | * Update current child MBR info, and propagate info in parent. |
1336 | | * |
1337 | | * nBlockPtr is passed only to validate the consistency of the tree. |
1338 | | **********************************************************************/ |
1339 | | void TABMAPIndexBlock::UpdateCurChildMBR(GInt32 nXMin, GInt32 nYMin, |
1340 | | GInt32 nXMax, GInt32 nYMax, |
1341 | | CPL_UNUSED GInt32 nBlockPtr) |
1342 | 129 | { |
1343 | 129 | CPLAssert(m_poCurChild); |
1344 | 129 | CPLAssert(m_asEntries[m_nCurChildIndex].nBlockPtr == nBlockPtr); |
1345 | | |
1346 | 129 | if (m_asEntries[m_nCurChildIndex].XMin == nXMin && |
1347 | 129 | m_asEntries[m_nCurChildIndex].YMin == nYMin && |
1348 | 129 | m_asEntries[m_nCurChildIndex].XMax == nXMax && |
1349 | 129 | m_asEntries[m_nCurChildIndex].YMax == nYMax) |
1350 | 24 | { |
1351 | 24 | return; /* Nothing changed... nothing to do */ |
1352 | 24 | } |
1353 | | |
1354 | 105 | m_bModified = TRUE; |
1355 | | |
1356 | 105 | m_asEntries[m_nCurChildIndex].XMin = nXMin; |
1357 | 105 | m_asEntries[m_nCurChildIndex].YMin = nYMin; |
1358 | 105 | m_asEntries[m_nCurChildIndex].XMax = nXMax; |
1359 | 105 | m_asEntries[m_nCurChildIndex].YMax = nYMax; |
1360 | | |
1361 | 105 | m_nMinX = 1000000000; |
1362 | 105 | m_nMinY = 1000000000; |
1363 | 105 | m_nMaxX = -1000000000; |
1364 | 105 | m_nMaxY = -1000000000; |
1365 | | |
1366 | 1.02k | for (int i = 0; i < m_numEntries; i++) |
1367 | 916 | { |
1368 | 916 | if (m_asEntries[i].XMin < m_nMinX) |
1369 | 122 | m_nMinX = m_asEntries[i].XMin; |
1370 | 916 | if (m_asEntries[i].XMax > m_nMaxX) |
1371 | 136 | m_nMaxX = m_asEntries[i].XMax; |
1372 | | |
1373 | 916 | if (m_asEntries[i].YMin < m_nMinY) |
1374 | 147 | m_nMinY = m_asEntries[i].YMin; |
1375 | 916 | if (m_asEntries[i].YMax > m_nMaxY) |
1376 | 136 | m_nMaxY = m_asEntries[i].YMax; |
1377 | 916 | } |
1378 | | |
1379 | 105 | if (m_poParentRef) |
1380 | 33 | m_poParentRef->UpdateCurChildMBR(m_nMinX, m_nMinY, m_nMaxX, m_nMaxY, |
1381 | 33 | GetNodeBlockPtr()); |
1382 | 105 | } |
1383 | | |
1384 | | /********************************************************************** |
1385 | | * TABMAPIndexBlock::SetMAPBlockManagerRef() |
1386 | | * |
1387 | | * Pass a reference to the block manager object for the file this |
1388 | | * block belongs to. The block manager will be used by this object |
1389 | | * when it needs to automatically allocate a new block. |
1390 | | **********************************************************************/ |
1391 | | void TABMAPIndexBlock::SetMAPBlockManagerRef(TABBinBlockManager *poBlockMgr) |
1392 | 106 | { |
1393 | 106 | m_poBlockManagerRef = poBlockMgr; |
1394 | 106 | } |
1395 | | |
1396 | | /********************************************************************** |
1397 | | * TABMAPIndexBlock::SetParentRef() |
1398 | | * |
1399 | | * Used to pass a reference to this node's parent. |
1400 | | **********************************************************************/ |
1401 | | void TABMAPIndexBlock::SetParentRef(TABMAPIndexBlock *poParent) |
1402 | 61 | { |
1403 | 61 | m_poParentRef = poParent; |
1404 | 61 | } |
1405 | | |
1406 | | /********************************************************************** |
1407 | | * TABMAPIndexBlock::SetCurChild() |
1408 | | * |
1409 | | * Used to transfer a child object from one node to another |
1410 | | **********************************************************************/ |
1411 | | void TABMAPIndexBlock::SetCurChild(std::unique_ptr<TABMAPIndexBlock> &&poChild, |
1412 | | int nChildIndex) |
1413 | 1 | { |
1414 | 1 | if (poChild) |
1415 | 1 | poChild->SetParentRef(this); |
1416 | 1 | m_poCurChild = std::move(poChild); |
1417 | 1 | m_nCurChildIndex = nChildIndex; |
1418 | 1 | } |
1419 | | |
1420 | | /********************************************************************** |
1421 | | * TABMAPIndexBlock::Dump() |
1422 | | * |
1423 | | * Dump block contents... available only in DEBUG mode. |
1424 | | **********************************************************************/ |
1425 | | #ifdef DEBUG |
1426 | | |
1427 | | void TABMAPIndexBlock::Dump(FILE *fpOut /*=NULL*/) |
1428 | | { |
1429 | | if (fpOut == nullptr) |
1430 | | fpOut = stdout; |
1431 | | |
1432 | | fprintf(fpOut, "----- TABMAPIndexBlock::Dump() -----\n"); |
1433 | | if (m_pabyBuf == nullptr) |
1434 | | { |
1435 | | fprintf(fpOut, "Block has not been initialized yet."); |
1436 | | } |
1437 | | else |
1438 | | { |
1439 | | fprintf(fpOut, "Index Block (type %d) at offset %d.\n", m_nBlockType, |
1440 | | m_nFileOffset); |
1441 | | fprintf(fpOut, " m_numEntries = %d\n", m_numEntries); |
1442 | | |
1443 | | /*------------------------------------------------------------- |
1444 | | * Loop through all entries, dumping each of them |
1445 | | *------------------------------------------------------------*/ |
1446 | | if (m_numEntries > 0) |
1447 | | ReadAllEntries(); |
1448 | | |
1449 | | for (int i = 0; i < m_numEntries; i++) |
1450 | | { |
1451 | | fprintf(fpOut, " %6d -> (%d, %d) - (%d, %d)\n", |
1452 | | m_asEntries[i].nBlockPtr, m_asEntries[i].XMin, |
1453 | | m_asEntries[i].YMin, m_asEntries[i].XMax, |
1454 | | m_asEntries[i].YMax); |
1455 | | } |
1456 | | } |
1457 | | |
1458 | | fflush(fpOut); |
1459 | | } |
1460 | | #endif // DEBUG |