Line data Source code
1 : /******************************************************************************
2 : *
3 : * Project: DTED Translator
4 : * Purpose: DTED Point Stream Writer.
5 : * Author: Frank Warmerdam, warmerdam@pobox.com
6 : *
7 : ******************************************************************************
8 : * Copyright (c) 2001, Frank Warmerdam
9 : *
10 : * SPDX-License-Identifier: MIT
11 : ****************************************************************************/
12 :
13 : #include "dted_api.h"
14 :
15 : typedef struct
16 : {
17 : char *pszFilename;
18 : DTEDInfo *psInfo;
19 :
20 : GInt16 **papanProfiles;
21 :
22 : int nLLLong;
23 : int nLLLat;
24 : } DTEDCachedFile;
25 :
26 : typedef struct
27 : {
28 : int nLevel;
29 : char *pszPath;
30 :
31 : double dfPixelSize;
32 :
33 : int nOpenFiles;
34 : DTEDCachedFile *pasCF;
35 :
36 : int nLastFile;
37 :
38 : char *apszMetadata[DTEDMD_MAX + 1];
39 : } DTEDPtStream;
40 :
41 : /************************************************************************/
42 : /* DTEDCreatePtStream() */
43 : /************************************************************************/
44 :
45 0 : void *DTEDCreatePtStream(const char *pszPath, int nLevel)
46 :
47 : {
48 : DTEDPtStream *psStream;
49 : int i;
50 : VSIStatBuf sStat;
51 :
52 : /* -------------------------------------------------------------------- */
53 : /* Does the target directory already exist? If not try to */
54 : /* create it. */
55 : /* -------------------------------------------------------------------- */
56 0 : if (CPLStat(pszPath, &sStat) != 0)
57 : {
58 0 : if (VSIMkdir(pszPath, 0755) != 0)
59 : {
60 : #ifndef AVOID_CPL
61 0 : CPLError(CE_Failure, CPLE_OpenFailed,
62 : "Unable to find, or create directory `%s'.", pszPath);
63 : #endif
64 0 : return NULL;
65 : }
66 : }
67 :
68 : /* -------------------------------------------------------------------- */
69 : /* Create the stream and initialize it. */
70 : /* -------------------------------------------------------------------- */
71 :
72 0 : psStream = (DTEDPtStream *)CPLCalloc(sizeof(DTEDPtStream), 1);
73 0 : psStream->nLevel = nLevel;
74 0 : psStream->pszPath = CPLStrdup(pszPath);
75 0 : psStream->nOpenFiles = 0;
76 0 : psStream->pasCF = NULL;
77 0 : psStream->nLastFile = -1;
78 :
79 0 : for (i = 0; i < DTEDMD_MAX + 1; i++)
80 0 : psStream->apszMetadata[i] = NULL;
81 :
82 0 : if (nLevel == 0)
83 0 : psStream->dfPixelSize = 1.0 / 120.0;
84 0 : else if (nLevel == 1)
85 0 : psStream->dfPixelSize = 1.0 / 1200.0;
86 : else /* if( nLevel == 2 ) */
87 0 : psStream->dfPixelSize = 1.0 / 3600.0;
88 :
89 0 : return (void *)psStream;
90 : }
91 :
92 : /************************************************************************/
93 : /* DTEDPtStreamNewTile() */
94 : /* */
95 : /* Create a new DTED file file, add it to our list, and make it */
96 : /* "current". */
97 : /************************************************************************/
98 :
99 0 : static int DTEDPtStreamNewTile(DTEDPtStream *psStream, int nCrLong, int nCrLat)
100 :
101 : {
102 : DTEDInfo *psInfo;
103 : char szFile[128];
104 : char chNSHemi, chEWHemi;
105 : char *pszFullFilename;
106 : const char *pszError;
107 :
108 : /* work out filename */
109 0 : if (nCrLat < 0)
110 0 : chNSHemi = 's';
111 : else
112 0 : chNSHemi = 'n';
113 :
114 0 : if (nCrLong < 0)
115 0 : chEWHemi = 'w';
116 : else
117 0 : chEWHemi = 'e';
118 :
119 0 : snprintf(szFile, sizeof(szFile), "%c%03d%c%03d.dt%d", chEWHemi,
120 : ABS(nCrLong), chNSHemi, ABS(nCrLat), psStream->nLevel);
121 :
122 : pszFullFilename =
123 0 : CPLStrdup(CPLFormFilename(psStream->pszPath, szFile, NULL));
124 :
125 : /* create the dted file */
126 0 : pszError = DTEDCreate(pszFullFilename, psStream->nLevel, nCrLat, nCrLong);
127 0 : if (pszError != NULL)
128 : {
129 : #ifndef AVOID_CPL
130 0 : CPLError(CE_Failure, CPLE_OpenFailed,
131 : "Failed to create DTED file `%s'.\n%s", pszFullFilename,
132 : pszError);
133 : #endif
134 0 : return FALSE;
135 : }
136 :
137 0 : psInfo = DTEDOpen(pszFullFilename, "rb+", FALSE);
138 :
139 0 : if (psInfo == NULL)
140 : {
141 0 : CPLFree(pszFullFilename);
142 0 : return FALSE;
143 : }
144 :
145 : /* add cached file to stream */
146 0 : psStream->nOpenFiles++;
147 0 : psStream->pasCF = CPLRealloc(psStream->pasCF,
148 0 : sizeof(DTEDCachedFile) * psStream->nOpenFiles);
149 :
150 0 : psStream->pasCF[psStream->nOpenFiles - 1].psInfo = psInfo;
151 0 : psStream->pasCF[psStream->nOpenFiles - 1].papanProfiles =
152 0 : CPLCalloc(sizeof(GInt16 *), psInfo->nXSize);
153 0 : psStream->pasCF[psStream->nOpenFiles - 1].pszFilename = pszFullFilename;
154 0 : psStream->pasCF[psStream->nOpenFiles - 1].nLLLat = nCrLat;
155 0 : psStream->pasCF[psStream->nOpenFiles - 1].nLLLong = nCrLong;
156 :
157 0 : psStream->nLastFile = psStream->nOpenFiles - 1;
158 :
159 0 : return TRUE;
160 : }
161 :
162 : /************************************************************************/
163 : /* DTEDWritePtLL() */
164 : /************************************************************************/
165 :
166 0 : static int DTEDWritePtLL(CPL_UNUSED DTEDPtStream *psStream,
167 : DTEDCachedFile *psCF, double dfLong, double dfLat,
168 : double dfElev)
169 : {
170 : /* -------------------------------------------------------------------- */
171 : /* Determine what profile this belongs in, and initialize the */
172 : /* profile if it doesn't already exist. */
173 : /* -------------------------------------------------------------------- */
174 0 : DTEDInfo *psInfo = psCF->psInfo;
175 : int iProfile, i, iRow;
176 :
177 0 : iProfile = (int)((dfLong - psInfo->dfULCornerX) / psInfo->dfPixelSizeX);
178 0 : iProfile = MAX(0, MIN(psInfo->nXSize - 1, iProfile));
179 :
180 0 : if (psCF->papanProfiles[iProfile] == NULL)
181 : {
182 0 : psCF->papanProfiles[iProfile] =
183 0 : CPLMalloc(sizeof(GInt16) * psInfo->nYSize);
184 :
185 0 : for (i = 0; i < psInfo->nYSize; i++)
186 0 : psCF->papanProfiles[iProfile][i] = DTED_NODATA_VALUE;
187 : }
188 :
189 : /* -------------------------------------------------------------------- */
190 : /* Establish where we fit in the profile. */
191 : /* -------------------------------------------------------------------- */
192 0 : iRow = (int)((psInfo->dfULCornerY - dfLat) / psInfo->dfPixelSizeY);
193 0 : iRow = MAX(0, MIN(psInfo->nYSize - 1, iRow));
194 :
195 0 : psCF->papanProfiles[iProfile][iRow] = (GInt16)floor(dfElev + 0.5);
196 :
197 0 : return TRUE;
198 : }
199 :
200 : /************************************************************************/
201 : /* DTEDWritePt() */
202 : /* */
203 : /* Write a single point out, creating a new file if necessary */
204 : /* to hold it. */
205 : /************************************************************************/
206 :
207 0 : int DTEDWritePt(void *hStream, double dfLong, double dfLat, double dfElev)
208 :
209 : {
210 0 : DTEDPtStream *psStream = (DTEDPtStream *)hStream;
211 : int i;
212 : DTEDInfo *psInfo;
213 0 : int bOnBoundary = FALSE;
214 :
215 : /* -------------------------------------------------------------------- */
216 : /* Determine if we are in a boundary region ... that is in the */
217 : /* area of the edge "pixel" that is shared with adjacent */
218 : /* tiles. */
219 : /* -------------------------------------------------------------------- */
220 0 : if ((floor(dfLong - 0.5 * psStream->dfPixelSize) !=
221 0 : floor(dfLong + 0.5 * psStream->dfPixelSize)) ||
222 0 : (floor(dfLat - 0.5 * psStream->dfPixelSize) !=
223 0 : floor(dfLat + 0.5 * psStream->dfPixelSize)))
224 : {
225 0 : bOnBoundary = TRUE;
226 0 : psStream->nLastFile = -1;
227 : }
228 :
229 : /* ==================================================================== */
230 : /* Handle case where the tile is not on a boundary. We only */
231 : /* need one output tile. */
232 : /* ==================================================================== */
233 : /* -------------------------------------------------------------------- */
234 : /* Is the last file used still applicable? */
235 : /* -------------------------------------------------------------------- */
236 0 : if (!bOnBoundary)
237 : {
238 0 : if (psStream->nLastFile != -1)
239 : {
240 0 : psInfo = psStream->pasCF[psStream->nLastFile].psInfo;
241 :
242 0 : if (dfLat > psInfo->dfULCornerY ||
243 0 : dfLat < psInfo->dfULCornerY - 1.0 - psInfo->dfPixelSizeY ||
244 0 : dfLong < psInfo->dfULCornerX ||
245 0 : dfLong > psInfo->dfULCornerX + 1.0 + psInfo->dfPixelSizeX)
246 0 : psStream->nLastFile = -1;
247 : }
248 :
249 : /* --------------------------------------------------------------------
250 : */
251 : /* Search for the file to write to. */
252 : /* --------------------------------------------------------------------
253 : */
254 0 : for (i = 0; i < psStream->nOpenFiles && psStream->nLastFile == -1; i++)
255 : {
256 0 : psInfo = psStream->pasCF[i].psInfo;
257 :
258 0 : if (!(dfLat > psInfo->dfULCornerY ||
259 0 : dfLat < psInfo->dfULCornerY - 1.0 - psInfo->dfPixelSizeY ||
260 0 : dfLong < psInfo->dfULCornerX ||
261 0 : dfLong > psInfo->dfULCornerX + 1.0 + psInfo->dfPixelSizeX))
262 : {
263 0 : psStream->nLastFile = i;
264 : }
265 : }
266 :
267 : /* --------------------------------------------------------------------
268 : */
269 : /* If none found, create a new file. */
270 : /* --------------------------------------------------------------------
271 : */
272 0 : if (psStream->nLastFile == -1)
273 : {
274 : int nCrLong, nCrLat;
275 :
276 0 : nCrLong = (int)floor(dfLong);
277 0 : nCrLat = (int)floor(dfLat);
278 :
279 0 : if (!DTEDPtStreamNewTile(psStream, nCrLong, nCrLat))
280 0 : return FALSE;
281 : }
282 :
283 : /* --------------------------------------------------------------------
284 : */
285 : /* Write data out to selected tile. */
286 : /* --------------------------------------------------------------------
287 : */
288 0 : return DTEDWritePtLL(psStream, psStream->pasCF + psStream->nLastFile,
289 : dfLong, dfLat, dfElev);
290 : }
291 :
292 : /* ==================================================================== */
293 : /* Handle case where we are on a boundary. We may be writing */
294 : /* the value to as many as four tiles. */
295 : /* ==================================================================== */
296 : else
297 : {
298 : int nLatMin, nLatMax, nLongMin, nLongMax;
299 : int nCrLong, nCrLat;
300 :
301 0 : nLongMin = (int)floor(dfLong - 0.5 * psStream->dfPixelSize);
302 0 : nLongMax = (int)floor(dfLong + 0.5 * psStream->dfPixelSize);
303 0 : nLatMin = (int)floor(dfLat - 0.5 * psStream->dfPixelSize);
304 0 : nLatMax = (int)floor(dfLat + 0.5 * psStream->dfPixelSize);
305 :
306 0 : for (nCrLong = nLongMin; nCrLong <= nLongMax; nCrLong++)
307 : {
308 0 : for (nCrLat = nLatMin; nCrLat <= nLatMax; nCrLat++)
309 : {
310 0 : psStream->nLastFile = -1;
311 :
312 : /* --------------------------------------------------------------------
313 : */
314 : /* Find this tile in our existing list. */
315 : /* --------------------------------------------------------------------
316 : */
317 0 : for (i = 0; i < psStream->nOpenFiles; i++)
318 : {
319 0 : if (psStream->pasCF[i].nLLLong == nCrLong &&
320 0 : psStream->pasCF[i].nLLLat == nCrLat)
321 : {
322 0 : psStream->nLastFile = i;
323 0 : break;
324 : }
325 : }
326 :
327 : /* --------------------------------------------------------------------
328 : */
329 : /* Create the tile if not found. */
330 : /* --------------------------------------------------------------------
331 : */
332 0 : if (psStream->nLastFile == -1)
333 : {
334 0 : if (!DTEDPtStreamNewTile(psStream, nCrLong, nCrLat))
335 0 : return FALSE;
336 : }
337 :
338 : /* --------------------------------------------------------------------
339 : */
340 : /* Write to the tile. */
341 : /* --------------------------------------------------------------------
342 : */
343 0 : if (!DTEDWritePtLL(psStream,
344 0 : psStream->pasCF + psStream->nLastFile,
345 : dfLong, dfLat, dfElev))
346 0 : return FALSE;
347 : }
348 : }
349 : }
350 :
351 0 : return TRUE;
352 : }
353 :
354 : /************************************************************************/
355 : /* DTEDClosePtStream() */
356 : /************************************************************************/
357 :
358 0 : void DTEDClosePtStream(void *hStream)
359 :
360 : {
361 0 : DTEDPtStream *psStream = (DTEDPtStream *)hStream;
362 : int iFile, iMD;
363 :
364 : /* -------------------------------------------------------------------- */
365 : /* Flush all DTED files. */
366 : /* -------------------------------------------------------------------- */
367 0 : for (iFile = 0; iFile < psStream->nOpenFiles; iFile++)
368 : {
369 : int iProfile;
370 0 : DTEDCachedFile *psCF = psStream->pasCF + iFile;
371 :
372 0 : for (iProfile = 0; iProfile < psCF->psInfo->nXSize; iProfile++)
373 : {
374 0 : if (psCF->papanProfiles[iProfile] != NULL)
375 : {
376 0 : DTEDWriteProfile(psCF->psInfo, iProfile,
377 0 : psCF->papanProfiles[iProfile]);
378 0 : CPLFree(psCF->papanProfiles[iProfile]);
379 : }
380 : }
381 :
382 0 : CPLFree(psCF->papanProfiles);
383 :
384 0 : for (iMD = 0; iMD <= DTEDMD_MAX; iMD++)
385 : {
386 0 : if (psStream->apszMetadata[iMD] != NULL)
387 0 : DTEDSetMetadata(psCF->psInfo, (DTEDMetaDataCode)iMD,
388 0 : psStream->apszMetadata[iMD]);
389 : }
390 :
391 0 : DTEDClose(psCF->psInfo);
392 : }
393 :
394 : /* -------------------------------------------------------------------- */
395 : /* Final cleanup. */
396 : /* -------------------------------------------------------------------- */
397 :
398 0 : for (iMD = 0; iMD < DTEDMD_MAX + 1; iMD++)
399 0 : CPLFree(psStream->apszMetadata[iMD]);
400 :
401 0 : CPLFree(psStream->pasCF);
402 0 : CPLFree(psStream->pszPath);
403 0 : CPLFree(psStream);
404 0 : }
405 :
406 : /************************************************************************/
407 : /* DTEDFillPixel() */
408 : /************************************************************************/
409 0 : static void DTEDFillPixel(DTEDInfo *psInfo, GInt16 **papanProfiles,
410 : GInt16 **papanDstProfiles, int iX, int iY,
411 : int nPixelSearchDist, float *pafKernel)
412 :
413 : {
414 0 : int nKernelWidth = 2 * nPixelSearchDist + 1;
415 : int nXMin, nXMax, nYMin, nYMax;
416 0 : double dfCoefSum = 0.0, dfValueSum = 0.0;
417 : int iXS, iYS;
418 :
419 0 : nXMin = MAX(0, iX - nPixelSearchDist);
420 0 : nXMax = MIN(psInfo->nXSize - 1, iX + nPixelSearchDist);
421 0 : nYMin = MAX(0, iY - nPixelSearchDist);
422 0 : nYMax = MIN(psInfo->nYSize - 1, iY + nPixelSearchDist);
423 :
424 0 : for (iXS = nXMin; iXS <= nXMax; iXS++)
425 : {
426 0 : GInt16 *panThisProfile = papanProfiles[iXS];
427 :
428 0 : if (panThisProfile == NULL)
429 0 : continue;
430 :
431 0 : for (iYS = nYMin; iYS <= nYMax; iYS++)
432 : {
433 0 : if (panThisProfile[iYS] != DTED_NODATA_VALUE)
434 : {
435 : int iXK, iYK;
436 : float fKernelCoef;
437 :
438 0 : iXK = iXS - iX + nPixelSearchDist;
439 0 : iYK = iYS - iY + nPixelSearchDist;
440 :
441 0 : fKernelCoef = pafKernel[iXK + iYK * nKernelWidth];
442 0 : dfCoefSum += fKernelCoef;
443 0 : dfValueSum += (double)fKernelCoef * (double)panThisProfile[iYS];
444 : }
445 : }
446 : }
447 :
448 0 : if (dfCoefSum == 0.0)
449 0 : papanDstProfiles[iX][iY] = DTED_NODATA_VALUE;
450 : else
451 0 : papanDstProfiles[iX][iY] = (GInt16)floor(dfValueSum / dfCoefSum + 0.5);
452 0 : }
453 :
454 : /************************************************************************/
455 : /* DTEDFillPtStream() */
456 : /* */
457 : /* Apply simple inverse distance interpolator to all no-data */
458 : /* pixels based on available values within the indicated search */
459 : /* distance (rectangular). */
460 : /************************************************************************/
461 :
462 0 : void DTEDFillPtStream(void *hStream, int nPixelSearchDist)
463 :
464 : {
465 0 : DTEDPtStream *psStream = (DTEDPtStream *)hStream;
466 : int iFile, nKernelWidth;
467 : float *pafKernel;
468 : int iX, iY;
469 :
470 : /* -------------------------------------------------------------------- */
471 : /* Setup inverse distance weighting kernel. */
472 : /* -------------------------------------------------------------------- */
473 0 : nKernelWidth = 2 * nPixelSearchDist + 1;
474 0 : pafKernel = (float *)CPLMalloc(sizeof(float) * nKernelWidth * nKernelWidth);
475 :
476 0 : for (iX = 0; iX < nKernelWidth; iX++)
477 : {
478 0 : for (iY = 0; iY < nKernelWidth; iY++)
479 : {
480 0 : pafKernel[iX + iY * nKernelWidth] =
481 0 : (float)(1.0 /
482 0 : sqrt((nPixelSearchDist - iX) * (nPixelSearchDist - iX) +
483 0 : (nPixelSearchDist - iY) *
484 0 : (nPixelSearchDist - iY)));
485 : }
486 : }
487 :
488 : /* ==================================================================== */
489 : /* Process each cached file. */
490 : /* ==================================================================== */
491 0 : for (iFile = 0; iFile < psStream->nOpenFiles; iFile++)
492 : {
493 0 : DTEDInfo *psInfo = psStream->pasCF[iFile].psInfo;
494 0 : GInt16 **papanProfiles = psStream->pasCF[iFile].papanProfiles;
495 : GInt16 **papanDstProfiles;
496 :
497 : papanDstProfiles =
498 0 : (GInt16 **)CPLCalloc(sizeof(GInt16 *), psInfo->nXSize);
499 :
500 : /* --------------------------------------------------------------------
501 : */
502 : /* Setup output image. */
503 : /* --------------------------------------------------------------------
504 : */
505 0 : for (iX = 0; iX < psInfo->nXSize; iX++)
506 : {
507 0 : papanDstProfiles[iX] =
508 0 : (GInt16 *)CPLMalloc(sizeof(GInt16) * psInfo->nYSize);
509 : }
510 :
511 : /* --------------------------------------------------------------------
512 : */
513 : /* Interpolate all missing values, and copy over available values.
514 : */
515 : /* --------------------------------------------------------------------
516 : */
517 0 : for (iX = 0; iX < psInfo->nXSize; iX++)
518 : {
519 0 : for (iY = 0; iY < psInfo->nYSize; iY++)
520 : {
521 0 : if (papanProfiles[iX] == NULL ||
522 0 : papanProfiles[iX][iY] == DTED_NODATA_VALUE)
523 : {
524 0 : DTEDFillPixel(psInfo, papanProfiles, papanDstProfiles, iX,
525 : iY, nPixelSearchDist, pafKernel);
526 : }
527 : else
528 : {
529 0 : papanDstProfiles[iX][iY] = papanProfiles[iX][iY];
530 : }
531 : }
532 : }
533 : /* --------------------------------------------------------------------
534 : */
535 : /* Push new values back into cache. */
536 : /* --------------------------------------------------------------------
537 : */
538 0 : for (iX = 0; iX < psInfo->nXSize; iX++)
539 : {
540 0 : CPLFree(papanProfiles[iX]);
541 0 : papanProfiles[iX] = papanDstProfiles[iX];
542 : }
543 :
544 0 : CPLFree(papanDstProfiles);
545 : }
546 :
547 0 : CPLFree(pafKernel);
548 0 : }
549 :
550 : /************************************************************************/
551 : /* DTEDPtStreamSetMetadata() */
552 : /************************************************************************/
553 :
554 0 : void DTEDPtStreamSetMetadata(void *hStream, DTEDMetaDataCode eCode,
555 : const char *pszValue)
556 :
557 : {
558 0 : DTEDPtStream *psStream = (DTEDPtStream *)hStream;
559 :
560 0 : if ((int)eCode >= 0 && eCode < DTEDMD_MAX + 1)
561 : {
562 0 : CPLFree(psStream->apszMetadata[eCode]);
563 0 : psStream->apszMetadata[eCode] = CPLStrdup(pszValue);
564 : }
565 0 : }
566 :
567 : /************************************************************************/
568 : /* DTEDPtStreamTrimEdgeOnlyTiles() */
569 : /* */
570 : /* Erase all tiles that only have boundary values set. */
571 : /************************************************************************/
572 :
573 0 : void DTEDPtStreamTrimEdgeOnlyTiles(void *hStream)
574 :
575 : {
576 0 : DTEDPtStream *psStream = (DTEDPtStream *)hStream;
577 : int iFile;
578 :
579 0 : for (iFile = psStream->nOpenFiles - 1; iFile >= 0; iFile--)
580 : {
581 0 : DTEDInfo *psInfo = psStream->pasCF[iFile].psInfo;
582 0 : GInt16 **papanProfiles = psStream->pasCF[iFile].papanProfiles;
583 0 : int iProfile, iPixel, bGotNonEdgeData = FALSE;
584 :
585 0 : for (iProfile = 1; iProfile < psInfo->nXSize - 1; iProfile++)
586 : {
587 0 : if (papanProfiles[iProfile] == NULL)
588 0 : continue;
589 :
590 0 : for (iPixel = 1; iPixel < psInfo->nYSize - 1; iPixel++)
591 : {
592 0 : if (papanProfiles[iProfile][iPixel] != DTED_NODATA_VALUE)
593 : {
594 0 : bGotNonEdgeData = TRUE;
595 0 : break;
596 : }
597 : }
598 : }
599 :
600 0 : if (bGotNonEdgeData)
601 0 : continue;
602 :
603 : /* Remove this tile */
604 :
605 0 : for (iProfile = 0; iProfile < psInfo->nXSize; iProfile++)
606 : {
607 0 : if (papanProfiles[iProfile] != NULL)
608 0 : CPLFree(papanProfiles[iProfile]);
609 : }
610 0 : CPLFree(papanProfiles);
611 :
612 0 : DTEDClose(psInfo);
613 :
614 0 : VSIUnlink(psStream->pasCF[iFile].pszFilename);
615 0 : CPLFree(psStream->pasCF[iFile].pszFilename);
616 :
617 0 : memmove(psStream->pasCF + iFile, psStream->pasCF + iFile + 1,
618 0 : sizeof(DTEDCachedFile) * (psStream->nOpenFiles - iFile - 1));
619 0 : psStream->nOpenFiles--;
620 : }
621 0 : }
|