Coverage Report

Created: 2025-06-09 07:07

/src/gdal/frmts/terragen/terragendataset.cpp
Line
Count
Source (jump to first uncovered line)
1
/******************************************************************************
2
 * terragendataset.cpp,v 1.2
3
 *
4
 * Project:  Terragen(tm) TER Driver
5
 * Purpose:  Reader for Terragen TER documents
6
 * Author:   Ray Gardener, Daylon Graphics Ltd.
7
 *
8
 * Portions of this module derived from GDAL drivers by
9
 * Frank Warmerdam, see http://www.gdal.org
10
11
 rcg    apr 19/06       Fixed bug with hf size being misread by one
12
                        if xpts/ypts tags not included in file.
13
                        Added Create() support.
14
                        Treat pixels as points.
15
16
 rcg    jun  6/07       Better heightscale/baseheight determination
17
                    when writing.
18
19
 *
20
 ******************************************************************************
21
 * Copyright (c) 2006-2007 Daylon Graphics Ltd.
22
 *
23
 * SPDX-License-Identifier: MIT
24
 ******************************************************************************
25
 */
26
27
/*
28
        Terragen format notes:
29
30
        Based on official Planetside specs.
31
32
        All distances along all three axes are in
33
        terrain units, which are 30m by default.
34
        If a SCAL chunk is present, however, it
35
        can indicate something other than 30.
36
        Note that uniform scaling should be used.
37
38
        The offset (base height) in the ALTW chunk
39
        is in terrain units, and the scale (height scale)
40
        is a normalized value using unsigned 16-bit notation.
41
        The physical terrain value for a read pixel is
42
        hv' = hv * scale / 65536 + offset.
43
        It still needs to be scaled by SCAL to
44
        get to meters.
45
46
        For writing:
47
48
        SCAL = gridpost distance in meters
49
        hv_px = hv_m / SCAL
50
        span_px = span_m / SCAL
51
        offset = see TerragenDataset::write_header()
52
        scale = see TerragenDataset::write_header()
53
        physical hv =
54
          (hv_px - offset) * 65536.0/scale
55
56
        We tell callers that:
57
58
        Elevations are Int16 when reading,
59
        and Float32 when writing. We need logical
60
        elevations when writing so that we can
61
        encode them with as much precision as possible
62
        when going down to physical 16-bit ints.
63
        Implementing band::SetScale/SetOffset won't work because
64
        it requires callers to know format write details.
65
        So we've added two Create() options that let the
66
        caller tell us the span's logical extent, and with
67
        those two values we can convert to physical pixels.
68
69
        band::GetUnitType() returns meters.
70
        band::GetScale() returns SCAL * (scale/65536)
71
        band::GetOffset() returns SCAL * offset
72
        ds::GetSpatialRef() returns a local CS
73
                using meters.
74
        ds::GetGeoTransform() returns a scale matrix
75
                having SCAL sx,sy members.
76
77
        ds::SetGeoTransform() lets us establish the
78
                size of ground pixels.
79
        ds::_SetProjection() lets us establish what
80
                units ground measures are in (also needed
81
                to calc the size of ground pixels).
82
        band::SetUnitType() tells us what units
83
                the given Float32 elevations are in.
84
        band::SetScale() is unused.
85
        band::SetOffset() is unused.
86
*/
87
88
#include "cpl_string.h"
89
#include "gdal_frmts.h"
90
#include "gdal_pam.h"
91
#include "ogr_spatialref.h"
92
93
#include <cmath>
94
95
#include <algorithm>
96
97
const double kdEarthCircumPolar = 40007849;
98
const double kdEarthCircumEquat = 40075004;
99
100
static double average(double a, double b)
101
0
{
102
0
    return 0.5 * (a + b);
103
0
}
104
105
static double degrees_to_radians(double d)
106
0
{
107
0
    return d * (M_PI / 180);
108
0
}
109
110
static bool approx_equal(double a, double b)
111
0
{
112
0
    const double epsilon = 1e-5;
113
0
    return std::abs(a - b) <= epsilon;
114
0
}
115
116
/************************************************************************/
117
/* ==================================================================== */
118
/*                              TerragenDataset                         */
119
/* ==================================================================== */
120
/************************************************************************/
121
122
class TerragenRasterBand;
123
124
class TerragenDataset final : public GDALPamDataset
125
{
126
    friend class TerragenRasterBand;
127
128
    double m_dScale, m_dOffset,
129
        m_dSCAL,  // 30.0 normally, from SCAL chunk
130
        m_adfTransform[6], m_dGroundScale, m_dMetersPerGroundUnit,
131
        m_dMetersPerElevUnit, m_dLogSpan[2], m_span_m[2], m_span_px[2];
132
133
    VSILFILE *m_fp;
134
    vsi_l_offset m_nDataOffset;
135
136
    GInt16 m_nHeightScale;
137
    GInt16 m_nBaseHeight;
138
139
    char *m_pszFilename;
140
    OGRSpatialReference m_oSRS{};
141
    char m_szUnits[32];
142
143
    bool m_bIsGeo;
144
145
    int LoadFromFile();
146
147
  public:
148
    TerragenDataset();
149
    virtual ~TerragenDataset();
150
151
    static GDALDataset *Open(GDALOpenInfo *);
152
    static GDALDataset *Create(const char *pszFilename, int nXSize, int nYSize,
153
                               int nBandsIn, GDALDataType eType,
154
                               char **papszOptions);
155
156
    virtual CPLErr GetGeoTransform(double *) override;
157
    virtual CPLErr SetGeoTransform(double *) override;
158
    const OGRSpatialReference *GetSpatialRef() const override;
159
    CPLErr SetSpatialRef(const OGRSpatialReference *poSRS) override;
160
161
  protected:
162
    bool get(GInt16 &);
163
    bool get(GUInt16 &);
164
    bool get(float &);
165
    bool put(GInt16);
166
    bool put(float);
167
168
    bool skip(size_t n)
169
0
    {
170
0
        return 0 == VSIFSeekL(m_fp, n, SEEK_CUR);
171
0
    }
172
173
    bool pad(size_t n)
174
0
    {
175
0
        return skip(n);
176
0
    }
177
178
    bool read_next_tag(char *);
179
    bool write_next_tag(const char *);
180
    static bool tag_is(const char *szTag, const char *);
181
182
    bool write_header(void);
183
};
184
185
/************************************************************************/
186
/* ==================================================================== */
187
/*                            TerragenRasterBand                        */
188
/* ==================================================================== */
189
/************************************************************************/
190
191
class TerragenRasterBand final : public GDALPamRasterBand
192
{
193
    friend class TerragenDataset;
194
195
    void *m_pvLine;
196
    bool m_bFirstTime;
197
198
  public:
199
    explicit TerragenRasterBand(TerragenDataset *);
200
201
    virtual ~TerragenRasterBand()
202
0
    {
203
0
        if (m_pvLine != nullptr)
204
0
            CPLFree(m_pvLine);
205
0
    }
206
207
    // Geomeasure support.
208
    virtual CPLErr IReadBlock(int, int, void *) override;
209
    virtual const char *GetUnitType() override;
210
    virtual double GetOffset(int *pbSuccess = nullptr) override;
211
    virtual double GetScale(int *pbSuccess = nullptr) override;
212
213
    virtual CPLErr IWriteBlock(int, int, void *) override;
214
    virtual CPLErr SetUnitType(const char *) override;
215
};
216
217
/************************************************************************/
218
/*                         TerragenRasterBand()                         */
219
/************************************************************************/
220
221
TerragenRasterBand::TerragenRasterBand(TerragenDataset *poDSIn)
222
0
    : m_pvLine(CPLMalloc(sizeof(GInt16) * poDSIn->GetRasterXSize())),
223
0
      m_bFirstTime(true)
224
0
{
225
0
    poDS = poDSIn;
226
0
    nBand = 1;
227
228
0
    eDataType = poDSIn->GetAccess() == GA_ReadOnly ? GDT_Int16 : GDT_Float32;
229
230
0
    nBlockXSize = poDSIn->GetRasterXSize();
231
0
    nBlockYSize = 1;
232
0
}
233
234
/************************************************************************/
235
/*                             IReadBlock()                             */
236
/************************************************************************/
237
238
CPLErr TerragenRasterBand::IReadBlock(CPL_UNUSED int nBlockXOff, int nBlockYOff,
239
                                      void *pImage)
240
0
{
241
    // CPLAssert( sizeof(float) == sizeof(GInt32) );
242
0
    CPLAssert(nBlockXOff == 0);
243
0
    CPLAssert(pImage != nullptr);
244
245
0
    TerragenDataset &ds = *reinterpret_cast<TerragenDataset *>(poDS);
246
247
    /* -------------------------------------------------------------------- */
248
    /*      Seek to scanline.
249
            Terragen is a bottom-top format, so we have to
250
            invert the row location.
251
     -------------------------------------------------------------------- */
252
0
    const size_t rowbytes = nBlockXSize * sizeof(GInt16);
253
254
0
    if (0 != VSIFSeekL(ds.m_fp,
255
0
                       ds.m_nDataOffset +
256
0
                           (ds.GetRasterYSize() - 1 - nBlockYOff) * rowbytes,
257
0
                       SEEK_SET))
258
0
    {
259
0
        CPLError(CE_Failure, CPLE_FileIO, "Terragen Seek failed:%s",
260
0
                 VSIStrerror(errno));
261
0
        return CE_Failure;
262
0
    }
263
264
    /* -------------------------------------------------------------------- */
265
    /*      Read the scanline into the line buffer.                        */
266
    /* -------------------------------------------------------------------- */
267
268
0
    if (VSIFReadL(pImage, rowbytes, 1, ds.m_fp) != 1)
269
0
    {
270
0
        CPLError(CE_Failure, CPLE_FileIO, "Terragen read failed:%s",
271
0
                 VSIStrerror(errno));
272
0
        return CE_Failure;
273
0
    }
274
275
/* -------------------------------------------------------------------- */
276
/*      Swap on MSB platforms.                                          */
277
/* -------------------------------------------------------------------- */
278
#ifdef CPL_MSB
279
    GDALSwapWords(pImage, sizeof(GInt16), nRasterXSize, sizeof(GInt16));
280
#endif
281
282
0
    return CE_None;
283
0
}
284
285
/************************************************************************/
286
/*                            GetUnitType()                             */
287
/************************************************************************/
288
const char *TerragenRasterBand::GetUnitType()
289
0
{
290
    // todo: Return elevation units.
291
    // For Terragen documents, it is the same as the ground units.
292
0
    TerragenDataset *poGDS = reinterpret_cast<TerragenDataset *>(poDS);
293
294
0
    return poGDS->m_szUnits;
295
0
}
296
297
/************************************************************************/
298
/*                              GetScale()                              */
299
/************************************************************************/
300
301
double TerragenRasterBand::GetScale(int *pbSuccess)
302
0
{
303
0
    const TerragenDataset &ds = *reinterpret_cast<TerragenDataset *>(poDS);
304
0
    if (pbSuccess != nullptr)
305
0
        *pbSuccess = TRUE;
306
307
0
    return ds.m_dScale;
308
0
}
309
310
/************************************************************************/
311
/*                             GetOffset()                              */
312
/************************************************************************/
313
314
double TerragenRasterBand::GetOffset(int *pbSuccess)
315
0
{
316
0
    const TerragenDataset &ds = *reinterpret_cast<TerragenDataset *>(poDS);
317
0
    if (pbSuccess != nullptr)
318
0
        *pbSuccess = TRUE;
319
320
0
    return ds.m_dOffset;
321
0
}
322
323
/************************************************************************/
324
/*                             IWriteBlock()                            */
325
/************************************************************************/
326
327
CPLErr TerragenRasterBand::IWriteBlock(CPL_UNUSED int nBlockXOff,
328
                                       int nBlockYOff, void *pImage)
329
0
{
330
0
    CPLAssert(nBlockXOff == 0);
331
0
    CPLAssert(pImage != nullptr);
332
0
    CPLAssert(m_pvLine != nullptr);
333
334
0
    const size_t pixelsize = sizeof(GInt16);
335
336
0
    TerragenDataset &ds = *reinterpret_cast<TerragenDataset *>(poDS);
337
0
    if (m_bFirstTime)
338
0
    {
339
0
        m_bFirstTime = false;
340
0
        ds.write_header();
341
0
        ds.m_nDataOffset = VSIFTellL(ds.m_fp);
342
0
    }
343
0
    const size_t rowbytes = nBlockXSize * pixelsize;
344
345
0
    GInt16 *pLine = reinterpret_cast<GInt16 *>(m_pvLine);
346
347
0
    if (0 == VSIFSeekL(ds.m_fp,
348
0
                       ds.m_nDataOffset +
349
                           // Terragen is Y inverted.
350
0
                           (ds.GetRasterYSize() - 1 - nBlockYOff) * rowbytes,
351
0
                       SEEK_SET))
352
0
    {
353
        // Convert each float32 to int16.
354
0
        float *pfImage = reinterpret_cast<float *>(pImage);
355
0
        for (size_t x = 0; x < static_cast<size_t>(nBlockXSize); x++)
356
0
        {
357
0
            const double f = pfImage[x] * ds.m_dMetersPerElevUnit / ds.m_dSCAL;
358
0
            const GInt16 hv = static_cast<GInt16>(
359
0
                (f - ds.m_nBaseHeight) * 65536.0 / ds.m_nHeightScale
360
0
                /*+ ds.m_nShift*/);
361
0
            pLine[x] = hv;
362
0
        }
363
364
#ifdef CPL_MSB
365
        GDALSwapWords(m_pvLine, pixelsize, nBlockXSize, pixelsize);
366
#endif
367
0
        if (1 == VSIFWriteL(m_pvLine, rowbytes, 1, ds.m_fp))
368
0
            return CE_None;
369
0
    }
370
371
0
    return CE_Failure;
372
0
}
373
374
CPLErr TerragenRasterBand::SetUnitType(const char *psz)
375
0
{
376
0
    TerragenDataset &ds = *reinterpret_cast<TerragenDataset *>(poDS);
377
378
0
    if (EQUAL(psz, "m"))
379
0
        ds.m_dMetersPerElevUnit = 1.0;
380
0
    else if (EQUAL(psz, "ft"))
381
0
        ds.m_dMetersPerElevUnit = 0.3048;
382
0
    else if (EQUAL(psz, "sft"))
383
0
        ds.m_dMetersPerElevUnit = 1200.0 / 3937.0;
384
0
    else
385
0
        return CE_Failure;
386
387
0
    return CE_None;
388
0
}
389
390
/************************************************************************/
391
/* ==================================================================== */
392
/*                          TerragenDataset                             */
393
/* ==================================================================== */
394
/************************************************************************/
395
396
/************************************************************************/
397
/*                          TerragenDataset()                           */
398
/************************************************************************/
399
400
TerragenDataset::TerragenDataset()
401
0
    : m_dScale(0.0), m_dOffset(0.0), m_dSCAL(30.0), m_dGroundScale(0.0),
402
0
      m_dMetersPerGroundUnit(1.0), m_dMetersPerElevUnit(1.0), m_fp(nullptr),
403
0
      m_nDataOffset(0), m_nHeightScale(0), m_nBaseHeight(0),
404
0
      m_pszFilename(nullptr), m_bIsGeo(false)
405
0
{
406
0
    m_oSRS.SetAxisMappingStrategy(OAMS_TRADITIONAL_GIS_ORDER);
407
408
0
    m_dLogSpan[0] = 0.0;
409
0
    m_dLogSpan[1] = 0.0;
410
411
0
    m_adfTransform[0] = 0.0;
412
0
    m_adfTransform[1] = m_dSCAL;
413
0
    m_adfTransform[2] = 0.0;
414
0
    m_adfTransform[3] = 0.0;
415
0
    m_adfTransform[4] = 0.0;
416
0
    m_adfTransform[5] = m_dSCAL;
417
0
    m_span_m[0] = 0.0;
418
0
    m_span_m[1] = 0.0;
419
0
    m_span_px[0] = 0.0;
420
0
    m_span_px[1] = 0.0;
421
0
    memset(m_szUnits, 0, sizeof(m_szUnits));
422
0
}
423
424
/************************************************************************/
425
/*                          ~TerragenDataset()                          */
426
/************************************************************************/
427
428
TerragenDataset::~TerragenDataset()
429
430
0
{
431
0
    FlushCache(true);
432
433
0
    CPLFree(m_pszFilename);
434
435
0
    if (m_fp != nullptr)
436
0
        VSIFCloseL(m_fp);
437
0
}
438
439
bool TerragenDataset::write_header()
440
0
{
441
0
    char szHeader[16];
442
    // cppcheck-suppress bufferNotZeroTerminated
443
0
    memcpy(szHeader, "TERRAGENTERRAIN ", sizeof(szHeader));
444
445
0
    if (1 != VSIFWriteL(reinterpret_cast<void *>(szHeader), sizeof(szHeader), 1,
446
0
                        m_fp))
447
0
    {
448
0
        CPLError(CE_Failure, CPLE_FileIO,
449
0
                 "Couldn't write to Terragen file %s.\n"
450
0
                 "Is file system full?",
451
0
                 m_pszFilename);
452
453
0
        return false;
454
0
    }
455
456
    // --------------------------------------------------------------------
457
    //      Write out the heightfield dimensions, etc.
458
    // --------------------------------------------------------------------
459
460
0
    const int nXSize = GetRasterXSize();
461
0
    const int nYSize = GetRasterYSize();
462
463
0
    write_next_tag("SIZE");
464
0
    put(static_cast<GInt16>(std::min(nXSize, nYSize) - 1));
465
0
    pad(sizeof(GInt16));
466
467
0
    if (nXSize != nYSize)
468
0
    {
469
0
        write_next_tag("XPTS");
470
0
        put(static_cast<GInt16>(nXSize));
471
0
        pad(sizeof(GInt16));
472
0
        write_next_tag("YPTS");
473
0
        put(static_cast<GInt16>(nYSize));
474
0
        pad(sizeof(GInt16));
475
0
    }
476
477
0
    if (m_bIsGeo)
478
0
    {
479
        /*
480
          With a geographic projection (degrees),
481
          m_dGroundScale will be in degrees and
482
          m_dMetersPerGroundUnit is undefined.
483
          So we're going to estimate a m_dMetersPerGroundUnit
484
          value here (i.e., meters per degree).
485
486
          We figure out the degree size of one
487
          pixel, and then the latitude degrees
488
          of the heightfield's center. The circumference of
489
          the latitude's great circle lets us know how
490
          wide the pixel is in meters, and we
491
          average that with the pixel's meter breadth,
492
          which is based on the polar circumference.
493
        */
494
495
        /* const double m_dDegLongPerPixel =
496
              fabs(m_adfTransform[1]); */
497
498
0
        const double m_dDegLatPerPixel = std::abs(m_adfTransform[5]);
499
500
        /* const double m_dCenterLongitude =
501
              m_adfTransform[0] +
502
              (0.5 * m_dDegLongPerPixel * (nXSize-1)); */
503
504
0
        const double m_dCenterLatitude =
505
0
            m_adfTransform[3] + (0.5 * m_dDegLatPerPixel * (nYSize - 1));
506
507
0
        const double dLatCircum =
508
0
            kdEarthCircumEquat *
509
0
            std::sin(degrees_to_radians(90.0 - m_dCenterLatitude));
510
511
0
        const double dMetersPerDegLongitude = dLatCircum / 360;
512
        /* const double dMetersPerPixelX =
513
              (m_dDegLongPerPixel / 360) * dLatCircum; */
514
515
0
        const double dMetersPerDegLatitude = kdEarthCircumPolar / 360;
516
        /* const double dMetersPerPixelY =
517
              (m_dDegLatPerPixel / 360) * kdEarthCircumPolar; */
518
519
0
        m_dMetersPerGroundUnit =
520
0
            average(dMetersPerDegLongitude, dMetersPerDegLatitude);
521
0
    }
522
523
0
    m_dSCAL = m_dGroundScale * m_dMetersPerGroundUnit;
524
525
0
    if (m_dSCAL != 30.0)
526
0
    {
527
0
        const float sc = static_cast<float>(m_dSCAL);
528
0
        write_next_tag("SCAL");
529
0
        put(sc);
530
0
        put(sc);
531
0
        put(sc);
532
0
    }
533
534
0
    if (!write_next_tag("ALTW"))
535
0
    {
536
0
        CPLError(CE_Failure, CPLE_FileIO,
537
0
                 "Couldn't write to Terragen file %s.\n"
538
0
                 "Is file system full?",
539
0
                 m_pszFilename);
540
541
0
        return false;
542
0
    }
543
544
    // Compute physical scales and offsets.
545
0
    m_span_m[0] = m_dLogSpan[0] * m_dMetersPerElevUnit;
546
0
    m_span_m[1] = m_dLogSpan[1] * m_dMetersPerElevUnit;
547
548
0
    m_span_px[0] = m_span_m[0] / m_dSCAL;
549
0
    m_span_px[1] = m_span_m[1] / m_dSCAL;
550
551
0
    const double span_px = m_span_px[1] - m_span_px[0];
552
0
    m_nHeightScale = static_cast<GInt16>(span_px);
553
0
    if (m_nHeightScale == 0)
554
0
        m_nHeightScale++;
555
556
// TODO(schwehr): Make static functions.
557
0
#define P2L_PX(n, hs, bh) (static_cast<double>(n) / 65536.0 * (hs) + (bh))
558
559
0
#define L2P_PX(n, hs, bh) (static_cast<int>(((n) - (bh)) * 65536.0 / (hs)))
560
561
    // Increase the heightscale until the physical span
562
    // fits within a 16-bit range. The smaller the logical span,
563
    // the more necessary this becomes.
564
0
    int hs = m_nHeightScale;
565
0
    int bh = 0;
566
0
    for (; hs <= 32767; hs++)
567
0
    {
568
0
        double prevdelta = 1.0e30;
569
0
        for (bh = -32768; bh <= 32767; bh++)
570
0
        {
571
0
            const int nValley = L2P_PX(m_span_px[0], hs, bh);
572
0
            if (nValley < -32768)
573
0
                continue;
574
0
            const int nPeak = L2P_PX(m_span_px[1], hs, bh);
575
0
            if (nPeak > 32767)
576
0
                continue;
577
578
            // now see how closely the baseheight gets
579
            // to the pixel span.
580
0
            const double d = P2L_PX(nValley, hs, bh);
581
0
            const double delta = std::abs(d - m_span_px[0]);
582
0
            if (delta < prevdelta)  // Converging?
583
0
                prevdelta = delta;
584
0
            else
585
0
            {
586
                // We're diverging, so use the previous bh
587
                // and stop looking.
588
0
                bh--;
589
0
                break;
590
0
            }
591
0
        }
592
0
        if (bh != 32768)
593
0
            break;
594
0
    }
595
0
    if (hs == 32768)
596
0
    {
597
0
        CPLError(CE_Failure, CPLE_FileIO,
598
0
                 "Couldn't write to Terragen file %s.\n"
599
0
                 "Cannot find adequate heightscale/baseheight combination.",
600
0
                 m_pszFilename);
601
602
0
        return false;
603
0
    }
604
605
0
    m_nHeightScale = static_cast<GInt16>(hs);
606
0
    m_nBaseHeight = static_cast<GInt16>(bh);
607
608
    // m_nHeightScale is the one that gives us the
609
    // widest use of the 16-bit space. However, there
610
    // might be larger heightscales that, even though
611
    // the reduce the space usage, give us a better fit
612
    // for preserving the span extents.
613
614
0
    return put(m_nHeightScale) && put(m_nBaseHeight);
615
0
}
616
617
/************************************************************************/
618
/*                                get()                                 */
619
/************************************************************************/
620
621
bool TerragenDataset::get(GInt16 &value)
622
0
{
623
0
    if (1 == VSIFReadL(&value, sizeof(value), 1, m_fp))
624
0
    {
625
0
        CPL_LSBPTR16(&value);
626
0
        return true;
627
0
    }
628
0
    return false;
629
0
}
630
631
bool TerragenDataset::get(GUInt16 &value)
632
0
{
633
0
    if (1 == VSIFReadL(&value, sizeof(value), 1, m_fp))
634
0
    {
635
0
        CPL_LSBPTR16(&value);
636
0
        return true;
637
0
    }
638
0
    return false;
639
0
}
640
641
bool TerragenDataset::get(float &value)
642
0
{
643
0
    if (1 == VSIFReadL(&value, sizeof(value), 1, m_fp))
644
0
    {
645
0
        CPL_LSBPTR32(&value);
646
0
        return true;
647
0
    }
648
0
    return false;
649
0
}
650
651
/************************************************************************/
652
/*                                put()                                 */
653
/************************************************************************/
654
655
bool TerragenDataset::put(GInt16 n)
656
0
{
657
0
    CPL_LSBPTR16(&n);
658
0
    return 1 == VSIFWriteL(&n, sizeof(n), 1, m_fp);
659
0
}
660
661
bool TerragenDataset::put(float f)
662
0
{
663
0
    CPL_LSBPTR32(&f);
664
0
    return 1 == VSIFWriteL(&f, sizeof(f), 1, m_fp);
665
0
}
666
667
/************************************************************************/
668
/*                              tag stuff                               */
669
/************************************************************************/
670
671
bool TerragenDataset::read_next_tag(char *szTag)
672
0
{
673
0
    return 1 == VSIFReadL(szTag, 4, 1, m_fp);
674
0
}
675
676
bool TerragenDataset::write_next_tag(const char *szTag)
677
0
{
678
0
    return 1 == VSIFWriteL(reinterpret_cast<void *>(const_cast<char *>(szTag)),
679
0
                           4, 1, m_fp);
680
0
}
681
682
bool TerragenDataset::tag_is(const char *szTag, const char *sz)
683
0
{
684
0
    return 0 == memcmp(szTag, sz, 4);
685
0
}
686
687
/************************************************************************/
688
/*                            LoadFromFile()                            */
689
/************************************************************************/
690
691
int TerragenDataset::LoadFromFile()
692
0
{
693
0
    m_dSCAL = 30.0;
694
0
    m_nDataOffset = 0;
695
696
0
    if (0 != VSIFSeekL(m_fp, 16, SEEK_SET))
697
0
        return FALSE;
698
699
0
    char szTag[4];
700
0
    if (!read_next_tag(szTag) || !tag_is(szTag, "SIZE"))
701
0
        return FALSE;
702
703
0
    GUInt16 nSize;
704
0
    if (!get(nSize) || !skip(2))
705
0
        return FALSE;
706
707
    // Set dimensions to SIZE chunk. If we don't
708
    // encounter XPTS/YPTS chunks, we can assume
709
    // the terrain to be square.
710
0
    GUInt16 xpts = nSize + 1;
711
0
    GUInt16 ypts = nSize + 1;
712
713
0
    while (read_next_tag(szTag))
714
0
    {
715
0
        if (tag_is(szTag, "XPTS"))
716
0
        {
717
0
            get(xpts);
718
0
            if (xpts < nSize || !skip(2))
719
0
                return FALSE;
720
0
            continue;
721
0
        }
722
723
0
        if (tag_is(szTag, "YPTS"))
724
0
        {
725
0
            get(ypts);
726
0
            if (ypts < nSize || !skip(2))
727
0
                return FALSE;
728
0
            continue;
729
0
        }
730
731
0
        if (tag_is(szTag, "SCAL"))
732
0
        {
733
0
            float sc[3] = {0.0f};
734
0
            get(sc[0]);
735
0
            get(sc[1]);
736
0
            get(sc[2]);
737
0
            m_dSCAL = sc[1];
738
0
            continue;
739
0
        }
740
741
0
        if (tag_is(szTag, "CRAD"))
742
0
        {
743
0
            if (!skip(sizeof(float)))
744
0
                return FALSE;
745
0
            continue;
746
0
        }
747
0
        if (tag_is(szTag, "CRVM"))
748
0
        {
749
0
            if (!skip(sizeof(GUInt32)))
750
0
                return FALSE;
751
0
            continue;
752
0
        }
753
0
        if (tag_is(szTag, "ALTW"))
754
0
        {
755
0
            get(m_nHeightScale);
756
0
            get(m_nBaseHeight);
757
0
            m_nDataOffset = VSIFTellL(m_fp);
758
0
            if (!skip(static_cast<size_t>(xpts) * static_cast<size_t>(ypts) *
759
0
                      sizeof(GInt16)))
760
0
                return FALSE;
761
0
            continue;
762
0
        }
763
0
        if (tag_is(szTag, "EOF "))
764
0
        {
765
0
            break;
766
0
        }
767
0
    }
768
769
0
    if (xpts == 0 || ypts == 0 || m_nDataOffset == 0)
770
0
        return FALSE;
771
772
0
    nRasterXSize = xpts;
773
0
    nRasterYSize = ypts;
774
775
    // todo: sanity check: do we have enough pixels?
776
777
    // Cache realworld scaling and offset.
778
0
    m_dScale = m_dSCAL / 65536 * m_nHeightScale;
779
0
    m_dOffset = m_dSCAL * m_nBaseHeight;
780
0
    strcpy(m_szUnits, "m");
781
782
    // Make our projection to have origin at the
783
    // NW corner, and groundscale to match elev scale
784
    // (i.e., uniform voxels).
785
0
    m_adfTransform[0] = 0.0;
786
0
    m_adfTransform[1] = m_dSCAL;
787
0
    m_adfTransform[2] = 0.0;
788
0
    m_adfTransform[3] = 0.0;
789
0
    m_adfTransform[4] = 0.0;
790
0
    m_adfTransform[5] = m_dSCAL;
791
792
    /* -------------------------------------------------------------------- */
793
    /*      Set projection.                                                 */
794
    /* -------------------------------------------------------------------- */
795
    // Terragen files as of Apr 2006 are partially georeferenced,
796
    // we can declare a local coordsys that uses meters.
797
0
    m_oSRS.SetLocalCS("Terragen world space");
798
0
    m_oSRS.SetLinearUnits("m", 1.0);
799
800
0
    return TRUE;
801
0
}
802
803
/************************************************************************/
804
/*                           SetSpatialRef()                            */
805
/************************************************************************/
806
807
CPLErr TerragenDataset::SetSpatialRef(const OGRSpatialReference *poSRS)
808
0
{
809
    // Terragen files aren't really georeferenced, but
810
    // we should get the projection's linear units so
811
    // that we can scale elevations correctly.
812
813
    // m_dSCAL = 30.0; // default
814
815
0
    m_oSRS.Clear();
816
0
    if (poSRS)
817
0
        m_oSRS = *poSRS;
818
819
    /* -------------------------------------------------------------------- */
820
    /*      Linear units.                                                   */
821
    /* -------------------------------------------------------------------- */
822
0
    m_bIsGeo = poSRS != nullptr && m_oSRS.IsGeographic() != FALSE;
823
0
    if (m_bIsGeo)
824
0
    {
825
        // The caller is using degrees. We need to convert
826
        // to meters, otherwise we can't derive a SCAL
827
        // value to scale elevations with.
828
0
        m_bIsGeo = true;
829
0
    }
830
0
    else
831
0
    {
832
0
        const double dfLinear = m_oSRS.GetLinearUnits();
833
834
0
        if (approx_equal(dfLinear, 0.3048))
835
0
            m_dMetersPerGroundUnit = 0.3048;
836
0
        else if (approx_equal(dfLinear, CPLAtof(SRS_UL_US_FOOT_CONV)))
837
0
            m_dMetersPerGroundUnit = CPLAtof(SRS_UL_US_FOOT_CONV);
838
0
        else
839
0
            m_dMetersPerGroundUnit = 1.0;
840
0
    }
841
842
0
    return CE_None;
843
0
}
844
845
/************************************************************************/
846
/*                          GetSpatialRef()                             */
847
/************************************************************************/
848
849
const OGRSpatialReference *TerragenDataset::GetSpatialRef() const
850
0
{
851
0
    return m_oSRS.IsEmpty() ? nullptr : &m_oSRS;
852
0
}
853
854
/************************************************************************/
855
/*                          SetGeoTransform()                           */
856
/************************************************************************/
857
858
CPLErr TerragenDataset::SetGeoTransform(double *padfGeoTransform)
859
0
{
860
0
    memcpy(m_adfTransform, padfGeoTransform, sizeof(m_adfTransform));
861
862
    // Average the projection scales.
863
0
    m_dGroundScale = average(fabs(m_adfTransform[1]), fabs(m_adfTransform[5]));
864
0
    return CE_None;
865
0
}
866
867
/************************************************************************/
868
/*                          GetGeoTransform()                           */
869
/************************************************************************/
870
871
CPLErr TerragenDataset::GetGeoTransform(double *padfTransform)
872
0
{
873
0
    memcpy(padfTransform, m_adfTransform, sizeof(m_adfTransform));
874
0
    return CE_None;
875
0
}
876
877
/************************************************************************/
878
/*                                Create()                                */
879
/************************************************************************/
880
GDALDataset *TerragenDataset::Create(const char *pszFilename, int nXSize,
881
                                     int nYSize, int nBandsIn,
882
                                     GDALDataType eType, char **papszOptions)
883
0
{
884
0
    TerragenDataset *poDS = new TerragenDataset();
885
886
0
    poDS->eAccess = GA_Update;
887
888
0
    poDS->m_pszFilename = CPLStrdup(pszFilename);
889
890
    // --------------------------------------------------------------------
891
    //      Verify input options.
892
    // --------------------------------------------------------------------
893
0
    const char *pszValue = CSLFetchNameValue(papszOptions, "MINUSERPIXELVALUE");
894
0
    if (pszValue != nullptr)
895
0
        poDS->m_dLogSpan[0] = CPLAtof(pszValue);
896
897
0
    pszValue = CSLFetchNameValue(papszOptions, "MAXUSERPIXELVALUE");
898
0
    if (pszValue != nullptr)
899
0
        poDS->m_dLogSpan[1] = CPLAtof(pszValue);
900
901
0
    if (poDS->m_dLogSpan[1] <= poDS->m_dLogSpan[0])
902
0
    {
903
0
        CPLError(CE_Failure, CPLE_AppDefined,
904
0
                 "Inverted, flat, or unspecified span for Terragen file.");
905
906
0
        delete poDS;
907
0
        return nullptr;
908
0
    }
909
910
0
    if (eType != GDT_Float32)
911
0
    {
912
0
        CPLError(CE_Failure, CPLE_AppDefined,
913
0
                 "Attempt to create Terragen dataset with a non-float32\n"
914
0
                 "data type (%s).\n",
915
0
                 GDALGetDataTypeName(eType));
916
917
0
        delete poDS;
918
0
        return nullptr;
919
0
    }
920
921
0
    if (nBandsIn != 1)
922
0
    {
923
0
        CPLError(CE_Failure, CPLE_NotSupported,
924
0
                 "Terragen driver doesn't support %d bands. Must be 1.\n",
925
0
                 nBandsIn);
926
927
0
        delete poDS;
928
0
        return nullptr;
929
0
    }
930
931
    // --------------------------------------------------------------------
932
    //      Try to create the file.
933
    // --------------------------------------------------------------------
934
935
0
    poDS->m_fp = VSIFOpenL(pszFilename, "wb+");
936
937
0
    if (poDS->m_fp == nullptr)
938
0
    {
939
0
        CPLError(CE_Failure, CPLE_OpenFailed,
940
0
                 "Attempt to create file `%s' failed.\n", pszFilename);
941
0
        delete poDS;
942
0
        return nullptr;
943
0
    }
944
945
0
    poDS->nRasterXSize = nXSize;
946
0
    poDS->nRasterYSize = nYSize;
947
948
    // Don't bother writing the header here; the first
949
    // call to IWriteBlock will do that instead, since
950
    // the elevation data's location depends on the
951
    // header size.
952
953
    // --------------------------------------------------------------------
954
    //      Instance a band.
955
    // --------------------------------------------------------------------
956
0
    poDS->SetBand(1, new TerragenRasterBand(poDS));
957
958
    // VSIFClose( poDS->m_fp );
959
960
    // return (GDALDataset *) GDALOpen( pszFilename, GA_Update );
961
0
    return GDALDataset::FromHandle(poDS);
962
0
}
963
964
/************************************************************************/
965
/*                                Open()                                */
966
/************************************************************************/
967
968
GDALDataset *TerragenDataset::Open(GDALOpenInfo *poOpenInfo)
969
970
16.2k
{
971
    // The file should have at least 32 header bytes
972
16.2k
    if (poOpenInfo->nHeaderBytes < 32 || poOpenInfo->fpL == nullptr)
973
666
        return nullptr;
974
975
15.5k
    if (!EQUALN(reinterpret_cast<const char *>(poOpenInfo->pabyHeader),
976
15.5k
                "TERRAGENTERRAIN ", 16))
977
15.5k
        return nullptr;
978
979
    /* -------------------------------------------------------------------- */
980
    /*      Create a corresponding GDALDataset.                             */
981
    /* -------------------------------------------------------------------- */
982
0
    TerragenDataset *poDS = new TerragenDataset();
983
0
    poDS->eAccess = poOpenInfo->eAccess;
984
0
    poDS->m_fp = poOpenInfo->fpL;
985
0
    poOpenInfo->fpL = nullptr;
986
987
    /* -------------------------------------------------------------------- */
988
    /*      Read the file.                                                  */
989
    /* -------------------------------------------------------------------- */
990
0
    if (!poDS->LoadFromFile())
991
0
    {
992
0
        delete poDS;
993
0
        return nullptr;
994
0
    }
995
996
    /* -------------------------------------------------------------------- */
997
    /*      Create band information objects.                                */
998
    /* -------------------------------------------------------------------- */
999
0
    poDS->SetBand(1, new TerragenRasterBand(poDS));
1000
1001
0
    poDS->SetMetadataItem(GDALMD_AREA_OR_POINT, GDALMD_AOP_POINT);
1002
1003
    /* -------------------------------------------------------------------- */
1004
    /*      Initialize any PAM information.                                 */
1005
    /* -------------------------------------------------------------------- */
1006
0
    poDS->SetDescription(poOpenInfo->pszFilename);
1007
0
    poDS->TryLoadXML();
1008
1009
    /* -------------------------------------------------------------------- */
1010
    /*      Support overviews.                                              */
1011
    /* -------------------------------------------------------------------- */
1012
0
    poDS->oOvManager.Initialize(poDS, poOpenInfo->pszFilename);
1013
1014
0
    return poDS;
1015
0
}
1016
1017
/************************************************************************/
1018
/*                        GDALRegister_Terragen()                       */
1019
/************************************************************************/
1020
1021
void GDALRegister_Terragen()
1022
1023
2
{
1024
2
    if (GDALGetDriverByName("Terragen") != nullptr)
1025
0
        return;
1026
1027
2
    GDALDriver *poDriver = new GDALDriver();
1028
1029
2
    poDriver->SetDescription("Terragen");
1030
2
    poDriver->SetMetadataItem(GDAL_DCAP_RASTER, "YES");
1031
2
    poDriver->SetMetadataItem(GDAL_DMD_EXTENSION, "ter");
1032
2
    poDriver->SetMetadataItem(GDAL_DMD_LONGNAME, "Terragen heightfield");
1033
2
    poDriver->SetMetadataItem(GDAL_DMD_HELPTOPIC,
1034
2
                              "drivers/raster/terragen.html");
1035
1036
2
    poDriver->SetMetadataItem(
1037
2
        GDAL_DMD_CREATIONOPTIONLIST,
1038
2
        "<CreationOptionList>"
1039
2
        "   <Option name='MINUSERPIXELVALUE' type='float' description='Lowest "
1040
2
        "logical elevation'/>"
1041
2
        "   <Option name='MAXUSERPIXELVALUE' type='float' description='Highest "
1042
2
        "logical elevation'/>"
1043
2
        "</CreationOptionList>");
1044
2
    poDriver->SetMetadataItem(GDAL_DCAP_VIRTUALIO, "YES");
1045
1046
2
    poDriver->pfnOpen = TerragenDataset::Open;
1047
2
    poDriver->pfnCreate = TerragenDataset::Create;
1048
1049
2
    GetGDALDriverManager()->RegisterDriver(poDriver);
1050
2
}