1 /******************************************************************************
2 * Copyright (c) 2011, Howard Butler, hobu.inc@gmail.com
3 *
4 * All rights reserved.
5 *
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following
8 * conditions are met:
9 *
10 *     * Redistributions of source code must retain the above copyright
11 *       notice, this list of conditions and the following disclaimer.
12 *     * Redistributions in binary form must reproduce the above copyright
13 *       notice, this list of conditions and the following disclaimer in
14 *       the documentation and/or other materials provided
15 *       with the distribution.
16 *     * Neither the name of Hobu, Inc. or Flaxen Geo Consulting nor the
17 *       names of its contributors may be used to endorse or promote
18 *       products derived from this software without specific prior
19 *       written permission.
20 *
21 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
28 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
29 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
31 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
32 * OF SUCH DAMAGE.
33 ****************************************************************************/
34 
35 /*
36 
37 Airborne Topographic Mapper (ATM) Project
38 NASA, Goddard Space Flight Center, Wallops Flight Facility
39 Principal Investigator: Bill Krabill (William.B.Krabill@nasa.gov)
40 
41 Description of ATM QFIT Output Data
42 (revised 2009-feb-13 sm)
43 
44 ATM data is generally distributed in the output format of the
45 processing  program, qfit, which combines airborne laser ranging
46 data and aircraft attitude from the INS with positioning
47 information from a processed kinematic  differential GPS
48 trajectory.  Qfit output files, which usually have names ending
49 in a .qi extension, are organized as 32-bit (4-byte)
50 binary  words, equivalent to a C or IDL long integer, which are
51 scaled to retain the precision of the measurements.  The format
52 and the scaling factors are presented below.  The qfit program is
53 run on an Apple PowerPC processor (and formerly Sun/Motorola).
54 Accordingly, the output is written in a big-endian format and
55 must be byte-swapped to be read with a PC (Intel processor)
56 which uses a little-endian format to store 32-bit integers.
57 
58 The files are organized into fixed-length logical records. The
59 beginning of the file contains a header of one or more records
60 followed by a data segment, in which there is one record per
61 laser shot. It is not necessary to interpret the header
62 to use the laser data->
63 
64 The first word of the header (and the file) is a 32-bit  binary
65 integer giving the number of bytes in each logical record.  Commonly
66 qfit files have 12 words per record and this integer will be the
67 number 48.  The remainder of the initial logical record is padded
68 with blank bytes (in this case 44 blank bytes). 10-word and 14-word
69 formats have also been used, as described below.
70 
71 The remainder of the header is generally a series of logical
72 records  containing the processing history of the file.  In these
73 logical records, the  initial word contains a 32-bit binary
74 integer with a value between -9000000 and -9000008.  The
75 remaining bytes in each header record is filled with a string of
76 ascii characters containing information on file processing
77 history.  In this case, the byte offset (as a longword integer)
78 from the start of file to the start of laser data will be the
79 second word of the second record of the header.  (Note: The header
80 records can be removed by eliminating records that begin with a
81 negative value since the first word of records in the data segment
82 is always a positive number.)
83 
84 In the data segment of the file, the information contained in
85 words 1-11 of the output record pertains to the  laser pulse, its
86 footprint, and aircraft attitude.  The last word of each record is
87 always the GPS time of day when the laser measurement was acquired.
88 
89 Prior to 2008 surveys, the GPS trajectory was edited to restrict PDOP<9
90 in order to limit GPS errors to be less than roughly 5cm.  The output
91 survey data would therefore have occasional gaps where the PDOP>9.
92 Some applications of ATM data have less stringent accuracy requirements
93 that would be better served by preserving the data in these gaps.
94 Starting in 2008, the PDOP limit was changed to 20, which could allow
95 occasional GPS errors up to about 15cm.  The PDOP value is carried in
96 the qfit output and can be used to edit data for applications requiring
97 greater precision.   Any file in the 10-word format, or files in the 12-word
98 format processed prior to January 2009, will have PDOP limited
99 <9.
100 
101 The three data formats are described below.  The format is designated by
102 the logical record length given in the first word of the data file.
103 
104 The qi 12-word format (in use since 2006):
105 Word #       Content
106    1    Relative Time (msec from start of data file)
107    2    Laser Spot Latitude (degrees X 1,000,000)
108    3    Laser Spot Longitude (degrees X 1,000,000)
109    4    Elevation (millimeters)
110    5    Start Pulse Signal Strength (relative)
111    6    Reflected Laser Signal Strength (relative)
112    7    Scan Azimuth (degrees X 1,000)
113    8    Pitch (degrees X 1,000)
114    9    Roll (degrees X 1,000)
115   10    GPS PDOP (dilution of precision) (X 10)
116   11    Laser received pulse width (digitizer samples)
117   12    GPS Time packed (example: 153320100 = 15h 33m 20s 100ms)
118 
119 10-word format (used prior to 2006):
120 Word #       Content
121    1    Relative Time (msec from start of data file)
122    2    Laser Spot Latitude (degrees X 1,000,000)
123    3    Laser Spot Longitude (degrees X 1,000,000)
124    4    Elevation (millimeters)
125    5    Start Pulse Signal Strength (relative)
126    6    Reflected Laser Signal Strength (relative)
127    7    Scan Azimuth (degrees X 1,000)
128    8    Pitch (degrees X 1,000)
129    9    Roll (degrees X 1,000)
130   10    GPS Time packed (example: 153320100 = 15h 33m 20s 100ms)
131 
132 
133 Between 1997 and 2004 some ATM surveys included
134 a separate sensor to measure passive brightness.
135 In the 14-word format, words 10-13 pertain to the
136 passive brightness signal, which is essentially a relative
137 measure of radiance reflected from the  earth's surface within
138 the vicinity of the laser pulse.  The horizontal position of the
139 passive footprint is determined relative to the laser footprint
140 by a  delay formulated during ground testing at Wallops.  The
141 elevation of the footprint is synthesized from surrounding laser
142 elevation data->  NOTE:  The passive data is not calibrated and
143 its use, if any, should  be qualitative in nature.  It may aid
144 the interpretation of terrain features. The measurement capability
145 was engineered into the ATM sensors to aid in the identification
146 of the water/beach interface acquired with  the instrument in
147 coastal mapping applications.
148 
149 14-word format:
150 Word #       Content
151    1    Relative Time (msec from start of data file)
152    2    Laser Spot Latitude (degrees X 1,000,000)
153    3    Laser Spot Longitude (degrees X 1,000,000)
154    4    Elevation (millimeters)
155    5    Start Pulse Signal Strength (relative)
156    6    Reflected Laser Signal Strength (relative)
157    7    Scan Azimuth (degrees X 1,000)
158    8    Pitch (degrees X 1,000)
159    9    Roll (degrees X 1,000)
160   10    Passive Signal (relative)
161   11    Passive Footprint Latitude (degrees X 1,000,000)
162   12    Passive Footprint Longitude (degrees X 1,000,000)
163   13    Passive Footprint Synthesized Elevation (millimeters)
164   14    GPS Time packed (example: 153320100 = 15h 33m 20s 100ms)
165 */
166 
167 #include "QfitReader.hpp"
168 
169 #include <pdal/PointView.hpp>
170 #include <pdal/util/Extractor.hpp>
171 #include <pdal/util/portable_endian.hpp>
172 #include <pdal/util/ProgramArgs.hpp>
173 
174 #include <algorithm>
175 #include <map>
176 
177 #pragma warning(disable: 4127)  // conditional expression is constant
178 
179 namespace pdal
180 {
181 
182 static StaticPluginInfo const s_info
183 {
184     "readers.qfit",
185     "QFIT Reader",
186     "http://pdal.io/stages/readers.qfit.html",
187     { "qi" }
188 };
189 
CREATE_STATIC_STAGE(QfitReader,s_info)190 CREATE_STATIC_STAGE(QfitReader, s_info)
191 
192 std::string QfitReader::getName() const { return s_info.name; }
193 
QfitReader()194 QfitReader::QfitReader()
195     : pdal::Reader()
196     , m_format(QFIT_Format_Unknown)
197     , m_size(0)
198     , m_littleEndian(false)
199     , m_istream()
200 {}
201 
202 
initialize()203 void QfitReader::initialize()
204 {
205     ISwitchableStream str(m_filename);
206     if (!str)
207         throwError("Unable to open file '" + m_filename + "'");
208     str.seek(0);
209 
210     int32_t int4(0);
211 
212     str >> int4;
213 
214     // They started writting little-endian data->
215 
216     /* For years we produced ATM data in big-endian format. With changes in
217     computer hardware, we reluctantly changed our standard output to
218     little-endian. The transition occurred between the two 2010 campaigns. The
219     format of the binary ( .qi, for example) files can be identified by
220     examining the first four bytes of the file. Read as a long integer (one
221     4-byte word), the value will contain the record length (in bytes) of the
222     data records. For example, a .qi file containing 14 words per record will
223     have a value 56 (=4*14). If the format of the file matches the format of
224     your processor, the value will be reasonable without byte-swapping. If the
225     format of the file differs from your processor, then the value is
226     interpreted as some very large number, unless you swap the byte order. If
227     you use Intel or equivalent processors, then you had to byte-swap files
228     from spring 2010 and earlier, you do not swap the ones from fall 2010 and
229     later. */
230 
231     // If the size comes back something other than 4*no_dimensions, we assume
232     // The data were flipped
233     if (int4 < 100)
234     {
235         m_littleEndian = true;
236     }
237     else
238     {
239         str.switchToBigEndian();
240     }
241 
242     if (!m_littleEndian)
243         int4 = int32_t(be32toh(uint32_t(int4)));
244 
245     if (int4 % 4 != 0)
246         throwError("Base QFIT format is not a multiple of 4, "
247             "unrecognized format!");
248 
249     m_size = int4;
250     m_format = static_cast<QFIT_Format_Type>(m_size / sizeof(m_size));
251 
252     // The offset to start reading point data should be here.
253     str.seek(m_size + sizeof(int4));
254 
255     str >> int4;
256     m_offset = static_cast<std::size_t>(int4);
257 
258     // Seek to the end
259     str.seek(0, std::istream::end);
260     std::ios::pos_type end = str.position();
261 
262     // First integer is the format of the file
263     std::ios::off_type offset = static_cast<std::ios::off_type>(m_offset);
264     m_point_bytes = end - offset;
265 }
266 
267 
addArgs(ProgramArgs & args)268 void QfitReader::addArgs(ProgramArgs& args)
269 {
270     args.add("flip_coordinates", "Flip coordinates from 0-360 to -180-180",
271         m_flip_x);
272     args.add("scale_z", "Z scale. Use 0.001 to go from mm to m",
273         m_scale_z, 0.001);
274 }
275 
276 
addDimensions(PointLayoutPtr layout)277 void QfitReader::addDimensions(PointLayoutPtr layout)
278 {
279     using namespace Dimension;
280 
281     m_size = 0;
282     layout->registerDim(Id::OffsetTime);
283     layout->registerDim(Id::Y);
284     layout->registerDim(Id::X);
285     layout->registerDim(Id::Z);
286     layout->registerDim(Id::StartPulse);
287     layout->registerDim(Id::ReflectedPulse);
288     layout->registerDim(Id::Azimuth);
289     layout->registerDim(Id::Pitch);
290     layout->registerDim(Id::Roll);
291     m_size += 36;
292 
293     if (m_format == QFIT_Format_12)
294     {
295         layout->registerDim(Id::Pdop);
296         layout->registerDim(Id::PulseWidth);
297         m_size += 8;
298     }
299     else if (m_format == QFIT_Format_14)
300     {
301         layout->registerDim(Id::PassiveSignal);
302         layout->registerDim(Id::PassiveY);
303         layout->registerDim(Id::PassiveX);
304         layout->registerDim(Id::PassiveZ);
305         m_size += 16;
306     }
307     m_size += 4;  // For the GPS time that we currently discard.
308 }
309 
310 
ready(PointTableRef)311 void QfitReader::ready(PointTableRef)
312 {
313     m_numPoints = m_point_bytes / m_size;
314     if (m_point_bytes % m_size)
315         throwError("Error calculating file point count.  File size is "
316             "inconsistent with point size.");
317     m_index = 0;
318     m_istream.reset(new IStream(m_filename));
319     m_istream->seek(m_offset);
320 }
321 
322 
read(PointViewPtr data,point_count_t count)323 point_count_t QfitReader::read(PointViewPtr data, point_count_t count)
324 {
325     if (!m_istream->good())
326         throwError("Corrupted file/file read error.");
327     if (m_istream->stream()->eof())
328         throwError("End of file detected.");
329 
330     count = (std::min)(m_numPoints - m_index, count);
331     std::vector<char> buf(m_size);
332     PointId nextId = data->size();
333     point_count_t numRead = 0;
334     while (count--)
335     {
336         m_istream->get(buf);
337         SwitchableExtractor extractor(buf.data(), m_size, m_littleEndian);
338 
339         // always read the base fields
340         {
341             int32_t time, y, xi, z, start_pulse, reflected_pulse, scan_angle,
342                 pitch, roll;
343             extractor >> time >> y >> xi >> z >> start_pulse >>
344                 reflected_pulse >> scan_angle >> pitch >> roll;
345             double x = xi / 1000000.0;
346             if (m_flip_x && x > 180)
347                 x -= 360;
348 
349             data->setField(Dimension::Id::OffsetTime, nextId, time);
350             data->setField(Dimension::Id::Y, nextId, y / 1000000.0);
351             data->setField(Dimension::Id::X, nextId, x);
352             data->setField(Dimension::Id::Z, nextId, z * m_scale_z);
353             data->setField(Dimension::Id::StartPulse, nextId, start_pulse);
354             data->setField(Dimension::Id::ReflectedPulse, nextId,
355                 reflected_pulse);
356             data->setField(Dimension::Id::Azimuth, nextId,
357                 scan_angle / 1000.0);
358             data->setField(Dimension::Id::Pitch, nextId, pitch / 1000.0);
359             data->setField(Dimension::Id::Roll, nextId, roll / 1000.0);
360         }
361 
362         if (m_format == QFIT_Format_12)
363         {
364             int32_t pdop, pulse_width;
365             extractor >> pdop >> pulse_width;
366             data->setField(Dimension::Id::Pdop, nextId, pdop / 10.0);
367             data->setField(Dimension::Id::PulseWidth, nextId, pulse_width);
368         }
369         else if (m_format == QFIT_Format_14)
370         {
371             int32_t passive_signal, passive_y, passive_x, passive_z;
372             extractor >> passive_signal >> passive_y >> passive_x >> passive_z;
373             double x = passive_x / 1000000.0;
374             if (m_flip_x && x > 180)
375                 x -= 360;
376             data->setField(Dimension::Id::PassiveSignal, nextId, passive_signal);
377             data->setField(Dimension::Id::PassiveY, nextId,
378                 passive_y / 1000000.0);
379             data->setField(Dimension::Id::PassiveX, nextId, x);
380             data->setField(Dimension::Id::PassiveZ, nextId,
381                 passive_z * m_scale_z);
382         }
383         // GPS time is really a GPS offset from the start of the GPS day
384         // encoded in this odd way: 153320100 = 15 hours 33 minutes
385         // 20 seconds 100 milliseconds.
386         // Not sure why we have that AND the other offset time.  For now
387         // we'll just extract this time and drop it.
388         int32_t gpstime;
389         extractor >> gpstime;
390 
391         if (m_cb)
392             m_cb(*data, nextId);
393 
394         numRead++;
395         nextId++;
396     }
397     m_index += numRead;
398 
399     return numRead;
400 }
401 
402 
done(PointTableRef)403 void QfitReader::done(PointTableRef)
404 {
405     m_istream.reset();
406 }
407 
408 } // namespace pdal
409