source: ntrip/trunk/BNC/src/rinex/rnxobsfile.cpp@ 6719

Last change on this file since 6719 was 6719, checked in by stuerze, 7 years ago

Some mandatory RINEX v3 header lines in observation files are added. That's of interest especially for merging RINEX v3 files. Within RINEX files, which are generated from RTCM streams, these lines are empty because of missing information in RTCM. But as soon this information is available from station logs the respective lines will be filled as well.

File size: 39.6 KB
Line 
1// Part of BNC, a utility for retrieving decoding and
2// converting GNSS data streams from NTRIP broadcasters.
3//
4// Copyright (C) 2007
5// German Federal Agency for Cartography and Geodesy (BKG)
6// http://www.bkg.bund.de
7// Czech Technical University Prague, Department of Geodesy
8// http://www.fsv.cvut.cz
9//
10// Email: euref-ip@bkg.bund.de
11//
12// This program is free software; you can redistribute it and/or
13// modify it under the terms of the GNU General Public License
14// as published by the Free Software Foundation, version 2.
15//
16// This program is distributed in the hope that it will be useful,
17// but WITHOUT ANY WARRANTY; without even the implied warranty of
18// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19// GNU General Public License for more details.
20//
21// You should have received a copy of the GNU General Public License
22// along with this program; if not, write to the Free Software
23// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
24
25/* -------------------------------------------------------------------------
26 * BKG NTRIP Client
27 * -------------------------------------------------------------------------
28 *
29 * Class: t_rnxObsFile
30 *
31 * Purpose: Reads RINEX Observation File
32 *
33 * Author: L. Mervart
34 *
35 * Created: 24-Jan-2012
36 *
37 * Changes:
38 *
39 * -----------------------------------------------------------------------*/
40
41#include <iostream>
42#include <iomanip>
43#include <sstream>
44#include "rnxobsfile.h"
45#include "bncutils.h"
46#include "bnccore.h"
47#include "bncsettings.h"
48
49using namespace std;
50
51// Constructor
52////////////////////////////////////////////////////////////////////////////
53t_rnxObsHeader::t_rnxObsHeader() {
54 _usedSystems = "GREJCS";
55 _antNEU.ReSize(3); _antNEU = 0.0;
56 _antXYZ.ReSize(3); _antXYZ = 0.0;
57 _antBSG.ReSize(3); _antBSG = 0.0;
58 _xyz.ReSize(3); _xyz = 0.0;
59 _version = 0.0;
60 _interval = 0.0;
61 for (unsigned iPrn = 1; iPrn <= t_prn::MAXPRN_GPS; iPrn++) {
62 _wlFactorsL1[iPrn] = 1;
63 _wlFactorsL2[iPrn] = 1;
64 }
65}
66
67// Destructor
68////////////////////////////////////////////////////////////////////////////
69t_rnxObsHeader::~t_rnxObsHeader() {
70}
71
72// Read Header
73////////////////////////////////////////////////////////////////////////////
74t_irc t_rnxObsHeader::read(QTextStream* stream, int maxLines) {
75 _comments.clear();
76 int numLines = 0;
77 while ( stream->status() == QTextStream::Ok && !stream->atEnd() ) {
78 QString line = stream->readLine(); ++ numLines;
79 if (line.isEmpty()) {
80 continue;
81 }
82 if (line.indexOf("END OF FILE") != -1) {
83 break;
84 }
85 QString value = line.mid(0,60).trimmed();
86 QString key = line.mid(60).trimmed();
87 if (key == "END OF HEADER") {
88 break;
89 }
90 else if (key == "RINEX VERSION / TYPE") {
91 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
92 in >> _version;
93 }
94 else if (key == "MARKER NAME") {
95 _markerName = value;
96 }
97 else if (key == "MARKER NUMBER") {
98 _markerNumber = line.mid(0,20).trimmed();
99 }
100 else if (key == "ANT # / TYPE") {
101 _antennaNumber = line.mid( 0,20).trimmed();
102 _antennaName = line.mid(20,20).trimmed();
103 }
104 else if (key == "OBSERVER / AGENCY") {
105 _observer = line.mid( 0,20).trimmed();
106 _agency = line.mid(20,40).trimmed();
107 }
108 else if (key == "REC # / TYPE / VERS") {
109 _receiverNumber = line.mid( 0,20).trimmed();
110 _receiverType = line.mid(20,20).trimmed();
111 _receiverVersion = line.mid(40,20).trimmed();
112 }
113 else if (key == "INTERVAL") {
114 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
115 in >> _interval;
116 }
117 else if (key == "COMMENT") {
118 _comments << line.mid(0,60).trimmed();
119 }
120 else if (key == "WAVELENGTH FACT L1/2") {
121 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
122 int wlFactL1 = 0;
123 int wlFactL2 = 0;
124 int numSat = 0;
125 in >> wlFactL1 >> wlFactL2 >> numSat;
126 if (numSat == 0) {
127 for (unsigned iPrn = 1; iPrn <= t_prn::MAXPRN_GPS; iPrn++) {
128 _wlFactorsL1[iPrn] = wlFactL1;
129 _wlFactorsL2[iPrn] = wlFactL2;
130 }
131 }
132 else {
133 for (int ii = 0; ii < numSat; ii++) {
134 QString prn; in >> prn;
135 if (prn[0] == 'G') {
136 int iPrn;
137 readInt(prn, 1, 2, iPrn);
138 _wlFactorsL1[iPrn] = wlFactL1;
139 _wlFactorsL2[iPrn] = wlFactL2;
140 }
141 }
142 }
143 }
144 else if (key == "APPROX POSITION XYZ") {
145 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
146 in >> _xyz[0] >> _xyz[1] >> _xyz[2];
147 }
148 else if (key == "ANTENNA: DELTA H/E/N") {
149 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
150 in >> _antNEU[2] >> _antNEU[1] >> _antNEU[0];
151 }
152 else if (key == "ANTENNA: DELTA X/Y/Z") {
153 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
154 in >> _antXYZ[0] >> _antXYZ[1] >> _antXYZ[2];
155 }
156 else if (key == "ANTENNA: B.SIGHT XYZ") {
157 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
158 in >> _antBSG[0] >> _antBSG[1] >> _antBSG[2];
159 }
160 else if (key == "# / TYPES OF OBSERV") {
161 if (_version == 0.0) {
162 _version = t_rnxObsHeader::defaultRnxObsVersion2;
163 }
164 QTextStream* in = new QTextStream(value.toAscii(), QIODevice::ReadOnly);
165 int nTypes;
166 *in >> nTypes;
167 char sys0 = _usedSystems[0].toAscii();
168 _obsTypes[sys0].clear();
169 for (int ii = 0; ii < nTypes; ii++) {
170 if (ii > 0 && ii % 9 == 0) {
171 line = stream->readLine(); ++numLines;
172 delete in;
173 in = new QTextStream(line.left(60).toAscii(), QIODevice::ReadOnly);
174 }
175 QString hlp;
176 *in >> hlp;
177 _obsTypes[sys0].append(hlp);
178 }
179 for (int ii = 1; ii < _usedSystems.length(); ii++) {
180 char sysI = _usedSystems[ii].toAscii();
181 _obsTypes[sysI] = _obsTypes[sys0];
182 }
183 }
184 else if (key == "SYS / # / OBS TYPES") {
185 if (_version == 0.0) {
186 _version = t_rnxObsHeader::defaultRnxObsVersion3;
187 }
188 QTextStream* in = new QTextStream(value.toAscii(), QIODevice::ReadOnly);
189 char sys;
190 int nTypes;
191 *in >> sys >> nTypes;
192 _obsTypes[sys].clear();
193 for (int ii = 0; ii < nTypes; ii++) {
194 if (ii > 0 && ii % 13 == 0) {
195 line = stream->readLine(); ++numLines;
196 delete in;
197 in = new QTextStream(line.toAscii(), QIODevice::ReadOnly);
198 }
199 QString hlp;
200 *in >> hlp;
201 if (sys == 'C') {
202 hlp.replace('2', '1');
203 }
204 _obsTypes[sys].push_back(hlp);
205 }
206 delete in;
207 }
208 else if (key == "TIME OF FIRST OBS") {
209 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
210 int year, month, day, hour, min;
211 double sec;
212 in >> year >> month >> day >> hour >> min >> sec;
213 _startTime.set(year, month, day, hour, min, sec);
214 }
215 else if (key == "SYS / PHASE SHIFT"){
216 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
217 char sys;
218 QString obstype;
219 double shift;
220 in >> sys >> obstype >> shift;
221 if (obstype.size())
222 _phaseShifts.insert(sys, QPair<QString, double>(obstype, shift));
223 }
224 else if (key == "GLONASS COD/PHS/BIS"){
225 QTextStream in(value.toAscii(), QIODevice::ReadOnly);
226 for (int ii = 0; ii < 4; ii++) {
227 QString type;
228 double value;
229 in >> type >> value;
230 if (type.size())
231 _gloPhaseBiases[type] = value;
232 }
233 }
234 else if (key == "GLONASS SLOT / FRQ #") {
235 QTextStream* in = new QTextStream(value.toAscii(), QIODevice::ReadOnly);
236 int nSlots = 0;
237 *in >> nSlots;
238 for (int ii = 0; ii < nSlots; ii++) {
239 if (ii > 0 && ii % 8 == 0) {
240 line = stream->readLine(); ++numLines;
241 delete in;
242 in = new QTextStream(line.left(60).toAscii(), QIODevice::ReadOnly);
243 }
244 QString sat;
245 int slot;
246 *in >> sat >> slot;
247 t_prn prn;
248 prn.set(sat.toStdString());
249 if(sat.size())
250 _gloSlots[prn] = slot;
251 }
252 delete in;
253 }
254 if (maxLines > 0 && numLines == maxLines) {
255 break;
256 }
257 }
258
259 // set default observation types if empty in input file
260 // ----------------------------------------------------
261 if (_obsTypes.empty()) {
262 setDefault(_markerName, _version);
263 }
264
265 // Systems used
266 // ------------
267 _usedSystems.clear();
268 QMapIterator<char, QStringList> it(_obsTypes);
269 while (it.hasNext()) {
270 it.next();
271 _usedSystems += QChar(it.key());
272 }
273
274 return success;
275}
276
277// Set Default Header
278////////////////////////////////////////////////////////////////////////////
279void t_rnxObsHeader::setDefault(const QString& markerName, int version) {
280
281 _markerName = markerName;
282
283 if (version <= 2) {
284 _version = t_rnxObsHeader::defaultRnxObsVersion2;
285 }
286 else {
287 _version = t_rnxObsHeader::defaultRnxObsVersion3;
288 }
289
290 _comments << "Default set of observation types used";
291
292 _obsTypes.clear();
293 if (_version < 3.0) {
294 _obsTypes['G'] << "C1" << "P1" << "L1" << "S1"
295 << "C2" << "P2" << "L2" << "S2";
296 _obsTypes['R'] = _obsTypes['G'];
297 _obsTypes['E'] = _obsTypes['G'];
298 _obsTypes['J'] = _obsTypes['G'];
299 _obsTypes['S'] = _obsTypes['G'];
300 _obsTypes['C'] = _obsTypes['G'];
301 }
302 else {
303 _obsTypes['G'] << "C1C" << "L1C" << "S1C"
304 << "C2W" << "L2W" << "S2W"
305 << "C5" << "L5" << "S5";
306
307 _obsTypes['J'] = _obsTypes['G'];
308
309 _obsTypes['R'] << "C1C" << "L1C" << "S1C"
310 << "C2P" << "L2P" << "S2P";
311
312 _obsTypes['E'] << "C1" << "L1" << "S1"
313 << "C5" << "L5" << "S5"
314 << "C7" << "L7" << "S7"
315 << "C8" << "L8" << "S8";
316
317 _obsTypes['S'] << "C1" << "L1" << "S1"
318 << "C5" << "L5" << "S5";
319
320 _obsTypes['C'] << "C1" << "L1" << "S1"
321 << "C6" << "L6" << "S6"
322 << "C7" << "L7" << "S7";
323 }
324}
325
326// Copy header
327////////////////////////////////////////////////////////////////////////////
328void t_rnxObsHeader::set(const t_rnxObsHeader& header, int version,
329 const QStringList* useObsTypes) {
330
331 if (version <= 2) {
332 _version = t_rnxObsHeader::defaultRnxObsVersion2;
333 }
334 else {
335 _version = t_rnxObsHeader::defaultRnxObsVersion3;
336 }
337 _interval = header._interval;
338 _antennaNumber = header._antennaNumber;
339 _antennaName = header._antennaName;
340 _markerName = header._markerName;
341 _markerNumber = header._markerNumber;
342 _antNEU = header._antNEU;
343 _antXYZ = header._antXYZ;
344 _antBSG = header._antBSG;
345 _xyz = header._xyz;
346 _observer = header._observer;
347 _agency = header._agency;
348 _receiverNumber = header._receiverNumber;
349 _receiverType = header._receiverType;
350 _receiverVersion = header._receiverVersion;
351 _startTime = header._startTime;
352 _comments = header._comments;
353 _usedSystems = header._usedSystems;
354 if (_version >= 3.0) {
355 _phaseShifts = header._phaseShifts;
356 _gloPhaseBiases = header._gloPhaseBiases;
357 _gloSlots = header._gloSlots;
358 }
359 for (unsigned iPrn = 1; iPrn <= t_prn::MAXPRN_GPS; iPrn++) {
360 _wlFactorsL1[iPrn] = header._wlFactorsL1[iPrn];
361 _wlFactorsL2[iPrn] = header._wlFactorsL2[iPrn];
362 }
363
364 // Set observation types
365 // ---------------------
366 _obsTypes.clear();
367 if (!useObsTypes || useObsTypes->size() == 0) {
368 if (int(_version) == int(header._version)) {
369 _obsTypes = header._obsTypes;
370 }
371 else {
372 if (_version >= 3.0) {
373 for (int iSys = 0; iSys < header.numSys(); iSys++) {
374 char sys = header.system(iSys);
375 for (int iType = 0; iType < header.nTypes(sys); iType++) {
376 QString type = header.obsType(sys, iType, _version);
377 if (!_obsTypes[sys].contains(type)) {
378 _obsTypes[sys].push_back(type);
379 }
380 }
381 }
382 }
383 else {
384 for (int iSys = 0; iSys < header.numSys(); iSys++) {
385 char sys = header.system(iSys);
386 for (int iType = 0; iType < header.nTypes(sys); iType++) {
387 QString type = header.obsType(sys, iType, _version);
388 for (int jSys = 0; jSys < _usedSystems.length(); jSys++) {
389 char thisSys = _usedSystems[jSys].toAscii();
390 if (!_obsTypes[thisSys].contains(type)) {
391 _obsTypes[thisSys].push_back(type);
392 }
393 }
394 }
395 }
396 }
397 }
398 }
399 else {
400 for (int iType = 0; iType < useObsTypes->size(); iType++) {
401 if (useObsTypes->at(iType).indexOf(":") != -1) {
402 QStringList hlp = useObsTypes->at(iType).split(":", QString::SkipEmptyParts);
403 if (hlp.size() == 2 && hlp[0].length() == 1) {
404 if (_version >= 3.0) {
405 char sys = hlp[0][0].toAscii();
406 QString type = t_rnxObsFile::type2to3(sys, hlp[1]);
407 if (!_obsTypes[sys].contains(type)) {
408 _obsTypes[sys].push_back(type);
409 }
410 }
411 else {
412 for (int iSys = 0; iSys < _usedSystems.length(); iSys++) {
413 char sys = _usedSystems[iSys].toAscii();
414 QString type = t_rnxObsFile::type3to2(sys, hlp[1]);
415 if (!_obsTypes[sys].contains(type)) {
416 _obsTypes[sys].push_back(type);
417 }
418 }
419 }
420 }
421 }
422 else {
423 for (int iSys = 0; iSys < _usedSystems.length(); iSys++) {
424 char sys = _usedSystems[iSys].toAscii();
425 QString type = _version >= 3.0 ? t_rnxObsFile::type2to3(sys, useObsTypes->at(iType)) :
426 t_rnxObsFile::type3to2(sys, useObsTypes->at(iType));
427 if (!_obsTypes[sys].contains(type)) {
428 _obsTypes[sys].push_back(type);
429 }
430 }
431 }
432 }
433 _usedSystems.clear();
434 QMapIterator<char, QStringList> it(_obsTypes);
435 while (it.hasNext()) {
436 it.next();
437 _usedSystems += QChar(it.key());
438 }
439 }
440}
441
442// Write Header
443////////////////////////////////////////////////////////////////////////////
444void t_rnxObsHeader::write(QTextStream* stream,
445 const QMap<QString, QString>* txtMap) const {
446
447 QStringList newComments;
448 QString runBy = BNC_CORE->userName();
449
450 if (txtMap) {
451 QMapIterator<QString, QString> it(*txtMap);
452 while (it.hasNext()) {
453 it.next();
454 if (it.key() == "RUN BY") {
455 runBy = it.value();
456 }
457 else if (it.key() == "COMMENT") {
458 newComments = it.value().split("\\n", QString::SkipEmptyParts);
459 }
460 }
461 }
462
463 *stream << QString("%1 Observation data Mixed")
464 .arg(_version, 9, 'f', 2)
465 .leftJustified(60)
466 << "RINEX VERSION / TYPE\n";
467
468 const QString fmtDate = (_version < 3.0) ? "dd-MMM-yy hh:mm"
469 : "yyyyMMdd hhmmss UTC";
470 *stream << QString("%1%2%3")
471 .arg(BNC_CORE->pgmName(), -20)
472 .arg(runBy.trimmed().left(20), -20)
473 .arg(QDateTime::currentDateTime().toUTC().toString(fmtDate), -20)
474 .leftJustified(60)
475 << "PGM / RUN BY / DATE\n";
476
477 QStringListIterator itCmnt(_comments + newComments);
478 while (itCmnt.hasNext()) {
479 *stream << itCmnt.next().trimmed().left(60).leftJustified(60) << "COMMENT\n";
480 }
481
482 *stream << QString("%1")
483 .arg(_markerName, -60)
484 .leftJustified(60)
485 << "MARKER NAME\n";
486
487 if (!_markerNumber.isEmpty()) {
488 *stream << QString("%1")
489 .arg(_markerNumber, -20)
490 .leftJustified(60)
491 << "MARKER NUMBER\n";
492 }
493
494 *stream << QString("%1%2")
495 .arg(_observer, -20)
496 .arg(_agency, -40)
497 .leftJustified(60)
498 << "OBSERVER / AGENCY\n";
499
500 *stream << QString("%1%2%3")
501 .arg(_receiverNumber, -20)
502 .arg(_receiverType, -20)
503 .arg(_receiverVersion, -20)
504 .leftJustified(60)
505 << "REC # / TYPE / VERS\n";
506
507 *stream << QString("%1%2")
508 .arg(_antennaNumber, -20)
509 .arg(_antennaName, -20)
510 .leftJustified(60)
511 << "ANT # / TYPE\n";
512
513 *stream << QString("%1%2%3")
514 .arg(_xyz(1), 14, 'f', 4)
515 .arg(_xyz(2), 14, 'f', 4)
516 .arg(_xyz(3), 14, 'f', 4)
517 .leftJustified(60)
518 << "APPROX POSITION XYZ\n";
519
520 *stream << QString("%1%2%3")
521 .arg(_antNEU(3), 14, 'f', 4)
522 .arg(_antNEU(2), 14, 'f', 4)
523 .arg(_antNEU(1), 14, 'f', 4)
524 .leftJustified(60)
525 << "ANTENNA: DELTA H/E/N\n";
526
527 if (_version < 3.0) {
528 int defaultWlFact1 = _wlFactorsL1[1];
529 int defaultWlFact2 = _wlFactorsL2[1]; // TODO check all prns
530 *stream << QString("%1%2")
531 .arg(defaultWlFact1, 6)
532 .arg(defaultWlFact2, 6)
533 .leftJustified(60)
534 << "WAVELENGTH FACT L1/2\n";
535 }
536
537 *stream << obsTypesStrings().join("");
538
539 if (_interval > 0) {
540 *stream << QString("%1")
541 .arg(_interval, 10, 'f', 3)
542 .leftJustified(60)
543 << "INTERVAL\n";
544 }
545
546 unsigned year, month, day, hour, min;
547 double sec;
548 _startTime.civil_date(year, month, day);
549 _startTime.civil_time(hour, min, sec);
550 *stream << QString("%1%2%3%4%5%6%7")
551 .arg(year, 6)
552 .arg(month, 6)
553 .arg(day, 6)
554 .arg(hour, 6)
555 .arg(min, 6)
556 .arg(sec, 13, 'f', 7)
557 .arg("GPS", 8)
558 .leftJustified(60)
559 << "TIME OF FIRST OBS\n";
560
561 if (_version >= 3.0) {
562 if (_phaseShifts.empty()) {
563 QString sys = _usedSystems;
564 for (int ii = 0; ii < sys.size(); ii++) {
565 *stream << QString("%1")
566 .arg(sys[ii], 0)
567 .leftJustified(60)
568 << "SYS / PHASE SHIFT\n";
569 }
570 } else {
571 QMultiHash<char, QPair<QString, double> >::const_iterator it = _phaseShifts.begin();
572 while(it != _phaseShifts.end()) {
573 *stream << QString("%1%2%3")
574 .arg(it.key(), 0)
575 .arg(it.value().first, 4)
576 .arg(it.value().second, 9, 'f', 5)
577 .leftJustified(60)
578 << "SYS / PHASE SHIFT\n";
579 it++;
580 }
581 }
582 }
583
584 if (_version >= 3.0) {
585 QString hlp = "";
586 QMap<QString, double>::const_iterator it = _gloPhaseBiases.begin();
587 while (it != _gloPhaseBiases.end()){
588 hlp += QString("%1%2").arg(it.key(), 4).arg(it.value(), 9, 'f', 3);
589 it++;
590 }
591 *stream << QString("%1")
592 .arg(hlp, 0)
593 .leftJustified(60)
594 << "GLONASS COD/PHS/BIS\n";
595 }
596
597 if (_version >= 3.0) {
598 QString number = QString::number(_gloSlots.size());
599 QString hlp = "";
600 int ii = 0;
601 QMap<t_prn, int>::const_iterator it = _gloSlots.begin();
602 while (it != _gloSlots.end()) {
603 QString prn(it.key().toString().c_str());
604 hlp += QString("%1%2").arg(prn, 4).arg(it.value(), 3);
605 it++;
606 ii++;
607 if (ii % 8 == 0) {
608 *stream << QString("%1%2")
609 .arg(number, 3)
610 .arg(hlp, 0)
611 .leftJustified(60)
612 << "GLONASS SLOT / FRQ #\n";
613 ii = 0;
614 hlp = number = "";
615 }
616 }
617 if (hlp.size() || !_gloSlots.size()) {
618 *stream << QString("%1%2")
619 .arg(number, 3)
620 .arg(hlp, 0)
621 .leftJustified(60)
622 << "GLONASS SLOT / FRQ #\n";
623 }
624 }
625
626
627 *stream << QString()
628 .leftJustified(60)
629 << "END OF HEADER\n";
630}
631
632// Number of Different Systems
633////////////////////////////////////////////////////////////////////////////
634int t_rnxObsHeader::numSys() const {
635 return _obsTypes.size();
636}
637
638//
639////////////////////////////////////////////////////////////////////////////
640char t_rnxObsHeader::system(int iSys) const {
641 int iSysLocal = -1;
642 QMapIterator<char, QStringList> it(_obsTypes);
643 while (it.hasNext()) {
644 ++iSysLocal;
645 it.next();
646 if (iSysLocal == iSys) {
647 return it.key();
648 }
649 }
650 return ' ';
651}
652
653// Number of Observation Types (satellite-system specific)
654////////////////////////////////////////////////////////////////////////////
655int t_rnxObsHeader::nTypes(char sys) const {
656 if (_obsTypes.contains(sys)) {
657 return _obsTypes[sys].size();
658 }
659 else {
660 return 0;
661 }
662}
663
664// Observation Type (satellite-system specific)
665////////////////////////////////////////////////////////////////////////////
666QString t_rnxObsHeader::obsType(char sys, int index, double version) const {
667
668 if (version == 0.0) {
669 version = _version;
670 }
671 if (_obsTypes.contains(sys)) {
672 QString origType = _obsTypes[sys].at(index);
673 if (int(version) == int(_version)) {
674 return origType;
675 }
676 else if (int(version) == 2) {
677 return t_rnxObsFile::type3to2(sys, origType);
678 }
679 else if (int(version) == 3) {
680 return t_rnxObsFile::type2to3(sys, origType);
681 }
682 }
683 return "";
684}
685
686// Write Observation Types
687////////////////////////////////////////////////////////////////////////////
688QStringList t_rnxObsHeader::obsTypesStrings() const {
689
690 QStringList strList;
691 if (_version < 3.0) {
692 char sys0 = _usedSystems[0].toAscii();
693 QString hlp;
694 QTextStream(&hlp) << QString("%1").arg(_obsTypes[sys0].size(), 6);
695 for (int ii = 0; ii < _obsTypes[sys0].size(); ii++) {
696 QTextStream(&hlp) << QString("%1").arg(_obsTypes[sys0][ii], 6);
697 if ((ii+1) % 9 == 0 || ii == _obsTypes[sys0].size()-1) {
698 strList.append(hlp.leftJustified(60) + "# / TYPES OF OBSERV\n");
699 hlp = QString().leftJustified(6);
700 }
701 }
702 }
703 else {
704 for (int iSys = 0; iSys < numSys(); iSys++) {
705 char sys = system(iSys);
706 QString hlp;
707 QTextStream(&hlp) << QString("%1 %2").arg(sys).arg(nTypes(sys), 3);
708 for (int iType = 0; iType < nTypes(sys); iType++) {
709 QString type = obsType(sys, iType);
710 QTextStream(&hlp) << QString(" %1").arg(type, -3);
711 if ((iType+1) % 13 == 0 || iType == nTypes(sys)-1) {
712 strList.append(hlp.leftJustified(60) + "SYS / # / OBS TYPES\n");
713 hlp = QString().leftJustified(6);
714 }
715 }
716 }
717 }
718
719 return strList;
720}
721
722// Constructor
723////////////////////////////////////////////////////////////////////////////
724t_rnxObsFile::t_rnxObsFile(const QString& fileName, e_inpOut inpOut) {
725 _inpOut = inpOut;
726 _stream = 0;
727 _flgPowerFail = false;
728 if (_inpOut == input) {
729 openRead(fileName);
730 }
731 else {
732 openWrite(fileName);
733 }
734}
735
736// Open for input
737////////////////////////////////////////////////////////////////////////////
738void t_rnxObsFile::openRead(const QString& fileName) {
739
740 _fileName = fileName; expandEnvVar(_fileName);
741 _file = new QFile(_fileName);
742 _file->open(QIODevice::ReadOnly | QIODevice::Text);
743 _stream = new QTextStream();
744 _stream->setDevice(_file);
745
746 _header.read(_stream);
747
748 // Guess Observation Interval
749 // --------------------------
750 if (_header._interval == 0.0) {
751 bncTime ttPrev;
752 for (int iEpo = 0; iEpo < 10; iEpo++) {
753 const t_rnxEpo* rnxEpo = nextEpoch();
754 if (!rnxEpo) {
755 throw QString("t_rnxObsFile: not enough epochs");
756 }
757 if (iEpo > 0) {
758 double dt = rnxEpo->tt - ttPrev;
759 if (_header._interval == 0.0 || dt < _header._interval) {
760 _header._interval = dt;
761 }
762 }
763 ttPrev = rnxEpo->tt;
764 }
765 _stream->seek(0);
766 _header.read(_stream);
767 }
768
769 // Time of first observation
770 // -------------------------
771 if (!_header._startTime.valid()) {
772 const t_rnxEpo* rnxEpo = nextEpoch();
773 if (!rnxEpo) {
774 throw QString("t_rnxObsFile: not enough epochs");
775 }
776 _header._startTime = rnxEpo->tt;
777 _stream->seek(0);
778 _header.read(_stream);
779 }
780}
781
782// Open for output
783////////////////////////////////////////////////////////////////////////////
784void t_rnxObsFile::openWrite(const QString& fileName) {
785
786 _fileName = fileName; expandEnvVar(_fileName);
787 _file = new QFile(_fileName);
788 _file->open(QIODevice::WriteOnly | QIODevice::Text);
789 _stream = new QTextStream();
790 _stream->setDevice(_file);
791}
792
793// Destructor
794////////////////////////////////////////////////////////////////////////////
795t_rnxObsFile::~t_rnxObsFile() {
796 close();
797}
798
799// Close
800////////////////////////////////////////////////////////////////////////////
801void t_rnxObsFile::close() {
802 delete _stream; _stream = 0;
803 delete _file; _file = 0;
804}
805
806// Handle Special Epoch Flag
807////////////////////////////////////////////////////////////////////////////
808void t_rnxObsFile::handleEpochFlag(int flag, const QString& line,
809 bool& headerReRead) {
810
811 headerReRead = false;
812
813 // Power Failure
814 // -------------
815 if (flag == 1) {
816 _flgPowerFail = true;
817 }
818
819 // Start moving antenna
820 // --------------------
821 else if (flag == 2) {
822 // no action
823 }
824
825 // Re-Read Header
826 // --------------
827 else if (flag == 3 || flag == 4 || flag == 5) {
828 int numLines = 0;
829 if (version() < 3.0) {
830 readInt(line, 29, 3, numLines);
831 }
832 else {
833 readInt(line, 32, 3, numLines);
834 }
835 if (flag == 3 || flag == 4) {
836 _header.read(_stream, numLines);
837 headerReRead = true;
838 }
839 else {
840 for (int ii = 0; ii < numLines; ii++) {
841 _stream->readLine();
842 }
843 }
844 }
845
846 // Unhandled Flag
847 // --------------
848 else {
849 throw QString("t_rnxObsFile: unhandled flag\n" + line);
850 }
851}
852
853// Retrieve single Epoch
854////////////////////////////////////////////////////////////////////////////
855t_rnxObsFile::t_rnxEpo* t_rnxObsFile::nextEpoch() {
856 _currEpo.clear();
857 if (version() < 3.0) {
858 return nextEpochV2();
859 }
860 else {
861 return nextEpochV3();
862 }
863}
864
865// Retrieve single Epoch (RINEX Version 3)
866////////////////////////////////////////////////////////////////////////////
867t_rnxObsFile::t_rnxEpo* t_rnxObsFile::nextEpochV3() {
868
869 while ( _stream->status() == QTextStream::Ok && !_stream->atEnd() ) {
870
871 QString line = _stream->readLine();
872
873 if (line.isEmpty()) {
874 continue;
875 }
876
877 int flag = 0;
878 readInt(line, 31, 1, flag);
879 if (flag > 0) {
880 bool headerReRead = false;
881 handleEpochFlag(flag, line, headerReRead);
882 if (headerReRead) {
883 continue;
884 }
885 }
886
887 QTextStream in(line.mid(1).toAscii(), QIODevice::ReadOnly);
888
889 // Epoch Time
890 // ----------
891 int year, month, day, hour, min;
892 double sec;
893 in >> year >> month >> day >> hour >> min >> sec;
894 _currEpo.tt.set(year, month, day, hour, min, sec);
895
896 // Number of Satellites
897 // --------------------
898 int numSat;
899 readInt(line, 32, 3, numSat);
900
901 _currEpo.rnxSat.resize(numSat);
902
903 // Observations
904 // ------------
905 for (int iSat = 0; iSat < numSat; iSat++) {
906 line = _stream->readLine();
907 t_prn prn; prn.set(line.left(3).toAscii().data());
908 _currEpo.rnxSat[iSat].prn = prn;
909 char sys = prn.system();
910 for (int iType = 0; iType < _header.nTypes(sys); iType++) {
911 int pos = 3 + 16*iType;
912 double obsValue = 0.0;
913 int lli = 0;
914 int snr = 0;
915 readDbl(line, pos, 14, obsValue);
916 readInt(line, pos + 14, 1, lli);
917 readInt(line, pos + 15, 1, snr);
918 if (_flgPowerFail) {
919 lli |= 1;
920 }
921 QString type = obsType(sys, iType);
922 _currEpo.rnxSat[iSat].obs[type].value = obsValue;
923 _currEpo.rnxSat[iSat].obs[type].lli = lli;
924 _currEpo.rnxSat[iSat].obs[type].snr = snr;
925 }
926 }
927
928 _flgPowerFail = false;
929
930 return &_currEpo;
931 }
932
933 return 0;
934}
935
936// Retrieve single Epoch (RINEX Version 2)
937////////////////////////////////////////////////////////////////////////////
938t_rnxObsFile::t_rnxEpo* t_rnxObsFile::nextEpochV2() {
939
940 while ( _stream->status() == QTextStream::Ok && !_stream->atEnd() ) {
941
942 QString line = _stream->readLine();
943
944 if (line.isEmpty()) {
945 continue;
946 }
947
948 int flag = 0;
949 readInt(line, 28, 1, flag);
950 if (flag > 0) {
951 bool headerReRead = false;
952 handleEpochFlag(flag, line, headerReRead);
953 if (headerReRead) {
954 continue;
955 }
956 }
957
958 QTextStream in(line.toAscii(), QIODevice::ReadOnly);
959
960 // Epoch Time
961 // ----------
962 int year, month, day, hour, min;
963 double sec;
964 in >> year >> month >> day >> hour >> min >> sec;
965 if (year < 80) {
966 year += 2000;
967 }
968 else if (year < 100) {
969 year += 1900;
970 }
971 _currEpo.tt.set(year, month, day, hour, min, sec);
972
973 // Number of Satellites
974 // --------------------
975 int numSat;
976 readInt(line, 29, 3, numSat);
977
978 _currEpo.rnxSat.resize(numSat);
979
980 // Read Satellite Numbers
981 // ----------------------
982 int pos = 32;
983 for (int iSat = 0; iSat < numSat; iSat++) {
984 if (iSat > 0 && iSat % 12 == 0) {
985 line = _stream->readLine();
986 pos = 32;
987 }
988
989 char sys = line.toAscii()[pos];
990 if (sys == ' ') {
991 sys = 'G';
992 }
993 int satNum; readInt(line, pos + 1, 2, satNum);
994 _currEpo.rnxSat[iSat].prn.set(sys, satNum);
995
996 pos += 3;
997 }
998
999 // Read Observation Records
1000 // ------------------------
1001 for (int iSat = 0; iSat < numSat; iSat++) {
1002 char sys = _currEpo.rnxSat[iSat].prn.system();
1003 line = _stream->readLine();
1004 pos = 0;
1005 for (int iType = 0; iType < _header.nTypes(sys); iType++) {
1006 if (iType > 0 && iType % 5 == 0) {
1007 line = _stream->readLine();
1008 pos = 0;
1009 }
1010 double obsValue = 0.0;
1011 int lli = 0;
1012 int snr = 0;
1013 readDbl(line, pos, 14, obsValue);
1014 readInt(line, pos + 14, 1, lli);
1015 readInt(line, pos + 15, 1, snr);
1016
1017 if (_flgPowerFail) {
1018 lli |= 1;
1019 }
1020
1021 QString type = obsType(sys, iType);
1022 _currEpo.rnxSat[iSat].obs[type].value = obsValue;
1023 _currEpo.rnxSat[iSat].obs[type].lli = lli;
1024 _currEpo.rnxSat[iSat].obs[type].snr = snr;
1025
1026 pos += 16;
1027 }
1028 }
1029
1030 _flgPowerFail = false;
1031
1032 return &_currEpo;
1033 }
1034
1035 return 0;
1036}
1037
1038// Write Data Epoch
1039////////////////////////////////////////////////////////////////////////////
1040void t_rnxObsFile::writeEpoch(const t_rnxEpo* epo) {
1041 if (epo == 0) {
1042 return;
1043 }
1044 t_rnxEpo epoLocal;
1045 epoLocal.tt = epo->tt;
1046 for (unsigned ii = 0; ii < epo->rnxSat.size(); ii++) {
1047 const t_rnxSat& rnxSat = epo->rnxSat[ii];
1048 if (_header._obsTypes[rnxSat.prn.system()].size() > 0) {
1049 epoLocal.rnxSat.push_back(rnxSat);
1050 }
1051 }
1052
1053 if (version() < 3.0) {
1054 return writeEpochV2(_stream, _header, &epoLocal);
1055 }
1056 else {
1057 return writeEpochV3(_stream, _header, &epoLocal);
1058 }
1059}
1060
1061// Write Data Epoch (RINEX Version 2)
1062////////////////////////////////////////////////////////////////////////////
1063void t_rnxObsFile::writeEpochV2(QTextStream* stream, const t_rnxObsHeader& header,
1064 const t_rnxEpo* epo) {
1065
1066 unsigned year, month, day, hour, min;
1067 double sec;
1068 epo->tt.civil_date(year, month, day);
1069 epo->tt.civil_time(hour, min, sec);
1070
1071 QString dateStr;
1072 QTextStream(&dateStr) << QString(" %1 %2 %3 %4 %5%6")
1073 .arg(int(fmod(year, 100)), 2, 10, QChar('0'))
1074 .arg(month, 2, 10, QChar('0'))
1075 .arg(day, 2, 10, QChar('0'))
1076 .arg(hour, 2, 10, QChar('0'))
1077 .arg(min, 2, 10, QChar('0'))
1078 .arg(sec, 11, 'f', 7);
1079
1080 int flag = 0;
1081 *stream << dateStr << QString("%1%2").arg(flag, 3).arg(epo->rnxSat.size(), 3);
1082 for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) {
1083 const t_rnxSat& rnxSat = epo->rnxSat[iSat];
1084 if (iSat > 0 && iSat % 12 == 0) {
1085 *stream << endl << QString().leftJustified(32);
1086 }
1087 *stream << rnxSat.prn.toString().c_str();
1088 }
1089 *stream << endl;
1090 for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) {
1091
1092 const t_rnxSat& rnxSat = epo->rnxSat[iSat];
1093 char sys = rnxSat.prn.system();
1094
1095 for (int iTypeV2 = 0; iTypeV2 < header.nTypes(sys); iTypeV2++) {
1096 if (iTypeV2 > 0 && iTypeV2 % 5 == 0) {
1097 *stream << endl;
1098 }
1099 QString typeV2 = header.obsType(sys, iTypeV2);
1100 bool found = false;
1101
1102 QString preferredAttrib = signalPriorities(sys);
1103 for (int iPref = 0; iPref < preferredAttrib.length(); iPref++) {
1104 QMapIterator<QString, t_rnxObs> itObs(rnxSat.obs);
1105 while (itObs.hasNext()) {
1106 itObs.next();
1107 const QString& type = itObs.key();
1108 const t_rnxObs& rnxObs = itObs.value();
1109 if ( preferredAttrib[iPref] == '?' ||
1110 (type.length() == 2 && preferredAttrib[iPref] == '_' ) ||
1111 (type.length() == 3 && preferredAttrib[iPref] == type[2]) ) {
1112 if (typeV2 == type3to2(sys, type)) {
1113 found = true;
1114 if (rnxObs.value == 0.0) {
1115 *stream << QString().leftJustified(16);
1116 }
1117 else {
1118 *stream << QString("%1").arg(rnxObs.value, 14, 'f', 3);
1119 if (rnxObs.lli != 0.0) {
1120 *stream << QString("%1").arg(rnxObs.lli,1);
1121 }
1122 else {
1123 *stream << ' ';
1124 }
1125 if (rnxObs.snr != 0.0) {
1126 *stream << QString("%1").arg(rnxObs.snr,1);
1127 }
1128 else {
1129 *stream << ' ';
1130 }
1131 }
1132 goto end_loop_iPref;
1133 }
1134 }
1135 }
1136 } end_loop_iPref:
1137 if (!found) {
1138 *stream << QString().leftJustified(16);
1139 }
1140 }
1141 *stream << endl;
1142 }
1143}
1144
1145// Write Data Epoch (RINEX Version 3)
1146////////////////////////////////////////////////////////////////////////////
1147void t_rnxObsFile::writeEpochV3(QTextStream* stream, const t_rnxObsHeader& header,
1148 const t_rnxEpo* epo) {
1149
1150 unsigned year, month, day, hour, min;
1151 double sec;
1152 epo->tt.civil_date(year, month, day);
1153 epo->tt.civil_time(hour, min, sec);
1154
1155 QString dateStr;
1156 QTextStream(&dateStr) << QString("> %1 %2 %3 %4 %5%6")
1157 .arg(year, 4)
1158 .arg(month, 2, 10, QChar('0'))
1159 .arg(day, 2, 10, QChar('0'))
1160 .arg(hour, 2, 10, QChar('0'))
1161 .arg(min, 2, 10, QChar('0'))
1162 .arg(sec, 11, 'f', 7);
1163
1164 int flag = 0;
1165 *stream << dateStr << QString("%1%2\n").arg(flag, 3).arg(epo->rnxSat.size(), 3);
1166
1167 for (unsigned iSat = 0; iSat < epo->rnxSat.size(); iSat++) {
1168 const t_rnxSat& rnxSat = epo->rnxSat[iSat];
1169 char sys = rnxSat.prn.system();
1170
1171 const t_rnxObs* hlp[header.nTypes(sys)];
1172 for (int iTypeV3 = 0; iTypeV3 < header.nTypes(sys); iTypeV3++) {
1173 hlp[iTypeV3] = 0;
1174 QString typeV3 = header.obsType(sys, iTypeV3);
1175 QMapIterator<QString, t_rnxObs> itObs(rnxSat.obs);
1176
1177 // Exact match
1178 // -----------
1179 while (itObs.hasNext()) {
1180 itObs.next();
1181 const QString& type = itObs.key();
1182 const t_rnxObs& rnxObs = itObs.value();
1183 if (typeV3 == type2to3(sys, type) && rnxObs.value != 0.0) {
1184 hlp[iTypeV3] = &itObs.value();
1185 }
1186 }
1187
1188 // Non-Exact match
1189 // ---------------
1190 itObs.toFront();
1191 while (itObs.hasNext()) {
1192 itObs.next();
1193 const QString& type = itObs.key();
1194 const t_rnxObs& rnxObs = itObs.value();
1195 if (hlp[iTypeV3] == 0 && typeV3 == type2to3(sys, type).left(2) && rnxObs.value != 0.0) {
1196 hlp[iTypeV3] = &itObs.value();
1197 }
1198 }
1199 }
1200
1201 *stream << rnxSat.prn.toString().c_str();
1202
1203 for (int iTypeV3 = 0; iTypeV3 < header.nTypes(sys); iTypeV3++) {
1204 const t_rnxObs* rnxObs = hlp[iTypeV3];
1205 if (rnxObs == 0) {
1206 *stream << QString().leftJustified(16);
1207 }
1208 else {
1209 *stream << QString("%1").arg(rnxObs->value, 14, 'f', 3);
1210 if (rnxObs->lli != 0.0) {
1211 *stream << QString("%1").arg(rnxObs->lli,1);
1212 }
1213 else {
1214 *stream << ' ';
1215 }
1216 if (rnxObs->snr != 0.0) {
1217 *stream << QString("%1").arg(rnxObs->snr,1);
1218 }
1219 else {
1220 *stream << ' ';
1221 }
1222 }
1223 }
1224 *stream << endl;
1225 }
1226}
1227
1228// Translate Observation Type v2 --> v3
1229////////////////////////////////////////////////////////////////////////////
1230QString t_rnxObsFile::type2to3(char sys, const QString& typeV2) {
1231 if (typeV2 == "P1") {
1232 return (sys == 'G') ? "C1W" : "C1P";
1233 }
1234 else if (typeV2 == "P2") {
1235 return (sys == 'G') ? "C2W" : "C2P";
1236 }
1237 return typeV2;
1238}
1239
1240// Translate Observation Type v3 --> v2
1241////////////////////////////////////////////////////////////////////////////
1242QString t_rnxObsFile::type3to2(char /* sys */, const QString& typeV3) {
1243 if (typeV3 == "C1P" || typeV3 == "C1W") {
1244 return "P1";
1245 }
1246 else if (typeV3 == "C2P" || typeV3 == "C2W") {
1247 return "P2";
1248 }
1249 return typeV3.left(2);
1250}
1251
1252// Set Observations from RINEX File
1253////////////////////////////////////////////////////////////////////////////
1254void t_rnxObsFile::setObsFromRnx(const t_rnxObsFile* rnxObsFile, const t_rnxObsFile::t_rnxEpo* epo,
1255 const t_rnxObsFile::t_rnxSat& rnxSat, t_satObs& obs) {
1256 obs._staID = rnxObsFile->markerName().toAscii().constData();
1257 obs._prn = rnxSat.prn;
1258 obs._time = epo->tt;
1259
1260 char sys = rnxSat.prn.system();
1261
1262 QChar addToL2;
1263 for (int iType = 0; iType < rnxObsFile->nTypes(sys); iType++) {
1264 QString type = rnxObsFile->obsType(sys, iType);
1265 QString typeV3 = rnxObsFile->obsType(sys, iType, 3.0); // may or may not differ from type
1266 if (rnxSat.obs.contains(type) && rnxSat.obs[type].value != 0.0) {
1267 if (type == "P2" && typeV3.length() > 2) {
1268 addToL2 = typeV3[2];
1269 break;
1270 }
1271 }
1272 }
1273
1274 for (int iType = 0; iType < rnxObsFile->nTypes(sys); iType++) {
1275 QString type = rnxObsFile->obsType(sys, iType);
1276 QString typeV3 = rnxObsFile->obsType(sys, iType, 3.0); // may or may not differ from type
1277 if (type == "L2") {
1278 typeV3 += addToL2;
1279 }
1280 if (rnxSat.obs.contains(type)) {
1281 const t_rnxObs& rnxObs = rnxSat.obs[type];
1282 if (rnxObs.value != 0.0) {
1283 string type2ch(typeV3.mid(1).toAscii().data());
1284
1285 t_frqObs* frqObs = 0;
1286 for (unsigned iFrq = 0; iFrq < obs._obs.size(); iFrq++) {
1287 if (obs._obs[iFrq]->_rnxType2ch == type2ch) {
1288 frqObs = obs._obs[iFrq];
1289 break;
1290 }
1291 }
1292 if (frqObs == 0) {
1293 frqObs = new t_frqObs;
1294 frqObs->_rnxType2ch = type2ch;
1295 obs._obs.push_back(frqObs);
1296 }
1297
1298 switch( typeV3.toAscii().data()[0] ) {
1299 case 'C':
1300 frqObs->_codeValid = true;
1301 frqObs->_code = rnxObs.value;
1302 break;
1303 case 'L':
1304 frqObs->_phaseValid = true;
1305 frqObs->_phase = rnxObs.value;
1306 frqObs->_slip = (rnxObs.lli & 1);
1307 break;
1308 case 'D':
1309 frqObs->_dopplerValid = true;
1310 frqObs->_doppler = rnxObs.value;
1311 break;
1312 case 'S':
1313 frqObs->_snrValid = true;
1314 frqObs->_snr = rnxObs.value;
1315 break;
1316 }
1317
1318 // Handle old-fashioned SNR values
1319 // -------------------------------
1320 if (rnxObs.snr != 0 && !frqObs->_snrValid) {
1321 frqObs->_snrValid = true;
1322 frqObs->_snr = rnxObs.snr * 6.0 + 2.5;
1323 }
1324 }
1325 }
1326 }
1327}
1328
1329// Tracking Mode Priorities
1330////////////////////////////////////////////////////////////////////////////
1331QString t_rnxObsFile::signalPriorities(char sys) {
1332
1333 bncSettings settings;
1334
1335 QStringList priorList;
1336 QString reqcAction = settings.value("reqcAction").toString();
1337
1338 // Priorities in Edit/Concatenate (post processing) mode
1339 // ---------------------------------------------------
1340 if (reqcAction == "Edit/Concatenate") {
1341 priorList = settings.value("reqcV2Priority").toString().split(" ", QString::SkipEmptyParts);
1342 }
1343
1344 // Priorities in real-time mode
1345 // ----------------------------
1346 else {
1347 priorList = settings.value("rnxV2Priority").toString().split(" ", QString::SkipEmptyParts);
1348 }
1349
1350 if (priorList.empty()) {
1351 priorList << "CWPX_?";
1352 }
1353
1354 QString result;
1355 for (int ii = 0; ii < priorList.size(); ii++) {
1356 if (priorList[ii].indexOf(":") != -1) {
1357 QStringList hlp = priorList[ii].split(":", QString::SkipEmptyParts);
1358 if (hlp.size() == 2 && hlp[0].length() == 1 && hlp[0][0] == sys) {
1359 result = hlp[1];
1360 break;
1361 }
1362 }
1363 else {
1364 result = priorList[ii];
1365 }
1366 }
1367
1368 return result;
1369}
Note: See TracBrowser for help on using the repository browser.