diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fd2d78adc..3260fc0bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ release. - Fixed gllssi2isis to support V1.1 data [#5396](https://github.com/DOI-USGS/ISIS3/issues/5396) ### Added +- Added option to save and apply bundle adjustment values in `jigsaw` [#4474](https://github.com/DOI-USGS/ISIS3/issues/4474) - Added versioned default values to lrowacphomap's PHOALGO and PHOPARCUBE parameters and updated lrowacphomap to handle them properly. [#5452](https://github.com/DOI-USGS/ISIS3/pull/5452) ## [8.2.0] - 2024-04-18 diff --git a/environment.yml b/environment.yml index 54e67abafe..9fca007e68 100644 --- a/environment.yml +++ b/environment.yml @@ -27,6 +27,7 @@ dependencies: - graphviz - conda-forge::gsl >=2.6, <2.7 - hdf5 + - highfive - icu - inja - jama diff --git a/isis/src/base/objs/SpiceRotation/SpiceRotation.cpp b/isis/src/base/objs/SpiceRotation/SpiceRotation.cpp index c5b6c55a59..c4af96fdb7 100644 --- a/isis/src/base/objs/SpiceRotation/SpiceRotation.cpp +++ b/isis/src/base/objs/SpiceRotation/SpiceRotation.cpp @@ -1880,6 +1880,11 @@ namespace Isis { const std::vector &coeffAng3, const Source type) { + if (type == PolyFunctionOverSpice && !m_orientation) { + QString msg = "The quaternion SPICE tables are no longer available. " + "Either re-run spiceinit or set OVEREXISTING to False."; + throw IException(IException::User, msg, _FILEINFO_); + } NaifStatus::CheckErrors(); Isis::PolynomialUnivariate function1(p_degree); Isis::PolynomialUnivariate function2(p_degree); @@ -3253,7 +3258,7 @@ namespace Isis { * @see SpiceRotation::SetEphemerisTime */ void SpiceRotation::setEphemerisTimeMemcache() { - // If the cache has only one rotation, set it + // If the cache has only one rotation, set it NaifStatus::CheckErrors(); if (p_cacheTime.size() == 1) { p_CJ = m_orientation->getRotations()[0].toRotationMatrix(); @@ -3487,23 +3492,23 @@ namespace Isis { p_av[index] += cacheVelocity[index]; } - if (angles[0] <= -1 * pi_c()) { - angles[0] += twopi_c(); - } - else if (angles[0] > pi_c()) { - angles[0] -= twopi_c(); - } + if (angles[0] <= -1 * pi_c()) { + angles[0] += twopi_c(); + } + else if (angles[0] > pi_c()) { + angles[0] -= twopi_c(); + } - if (angles[2] <= -1 * pi_c()) { - angles[2] += twopi_c(); - } - else if (angles[2] > pi_c()) { - angles[2] -= twopi_c(); - } + if (angles[2] <= -1 * pi_c()) { + angles[2] += twopi_c(); + } + else if (angles[2] > pi_c()) { + angles[2] -= twopi_c(); + } - eul2m_c((SpiceDouble) angles[2], (SpiceDouble) angles[1], (SpiceDouble) angles[0], - p_axis3, p_axis2, p_axis1, - (SpiceDouble( *)[3]) &p_CJ[0]); + eul2m_c((SpiceDouble) angles[2], (SpiceDouble) angles[1], (SpiceDouble) angles[0], + p_axis3, p_axis2, p_axis1, + (SpiceDouble( *)[3]) &p_CJ[0]); } diff --git a/isis/src/base/objs/Table/Table.cpp b/isis/src/base/objs/Table/Table.cpp index 330d422815..34b3aa1549 100644 --- a/isis/src/base/objs/Table/Table.cpp +++ b/isis/src/base/objs/Table/Table.cpp @@ -7,6 +7,7 @@ find files of those names at the top level of this repository. **/ #include "Table.h" #include +#include #include #include "Blob.h" @@ -137,6 +138,63 @@ namespace Isis { } } + /** + * This constructor takes in a string to create a Table object. + * + * @param tableName The name of the Table to be read + * @param tableStr The table string + * @param fieldDelimiter The delimiter to separate fields with + */ + Table::Table(const QString &tableName, const std::string &tableString, const char &fieldDelimiter) { + p_name = tableName; + + std::stringstream tableStream; + tableStream << tableString; + + std::vector tableLinesStringList; + std::string line; + while(std::getline(tableStream, line, '\n')) { + tableLinesStringList.push_back(line); + } + + int numOfFieldValues = tableLinesStringList.size() - 1; // minus the header line + + std::string fieldNamesLineString = tableLinesStringList.front(); + std::stringstream fieldNamesStringStream; + fieldNamesStringStream << fieldNamesLineString; + + std::vector fieldNames; + std::string fieldNameString; + while(std::getline(fieldNamesStringStream, fieldNameString, fieldDelimiter)) { + fieldNames.push_back(QString::fromStdString(fieldNameString)); + } + + // Clear error flags and set pointer back to beginning + tableStream.clear(); + tableStream.seekg(0, ios::beg); + + // Add records to table + std::string recordString; + int index = 0; + while(std::getline(tableStream, recordString, '\n')) { + // skip first line bc that's the header line + if (index == 0) { + index++; + continue; + } + + TableRecord tableRecord(recordString, fieldDelimiter, fieldNames, numOfFieldValues); + p_record = tableRecord; + this->operator+=(tableRecord); + index++; + } + + // Add fields + for (int f = 0; f < p_record.Fields(); f++) { + p_label.addGroup(p_record[f].pvlGroup()); + } + } + /** * Initialize a Table from a Blob that has been read from a file. diff --git a/isis/src/base/objs/Table/Table.h b/isis/src/base/objs/Table/Table.h index c5db233568..7179141ed6 100644 --- a/isis/src/base/objs/Table/Table.h +++ b/isis/src/base/objs/Table/Table.h @@ -79,6 +79,7 @@ namespace Isis { Table(const QString &tableName, const QString &file, const Pvl &fileHeader); Table(const Table &other); + Table(const QString &tableName, const std::string &tableString, const char &fieldDelimiter); Table &operator=(const Isis::Table &other); ~Table(); diff --git a/isis/src/base/objs/TableRecord/TableRecord.cpp b/isis/src/base/objs/TableRecord/TableRecord.cpp index 12384d46c4..7e72df9915 100644 --- a/isis/src/base/objs/TableRecord/TableRecord.cpp +++ b/isis/src/base/objs/TableRecord/TableRecord.cpp @@ -8,6 +8,7 @@ find files of those names at the top level of this repository. **/ #include "TableRecord.h" #include +#include #include #include @@ -21,6 +22,29 @@ namespace Isis { TableRecord::TableRecord(){ } + /** + * TableRecord constructor + * + * @param tableRecordStr Table record string + * @param fieldDelimiter The delimiter to separate fields with + * @param fieldNames Table header names + * @param numOfFieldValues Number of fields (rows) + */ + TableRecord::TableRecord(std::string tableRecordStr, char fieldDelimiter, + std::vector fieldNames, int numOfFieldValues) { + std::stringstream tableRecordStream; + tableRecordStream << tableRecordStr; + + std::string fieldStr; + int i = 0; + while(std::getline(tableRecordStream, fieldStr, fieldDelimiter)) { + TableField tableField(fieldNames[i], TableField::Double); + tableField = std::stod(fieldStr); // convert string to double + this->operator+=(tableField); + i++; + } + } + //! Destroys the TableRecord object TableRecord::~TableRecord() { } @@ -155,7 +179,7 @@ namespace Isis { Isis::TableField &field = p_fields[f]; field = (void *)&buf[sbyte]; sbyte += field.bytes(); - } + } } /** diff --git a/isis/src/base/objs/TableRecord/TableRecord.h b/isis/src/base/objs/TableRecord/TableRecord.h index ed610f38db..0df1a3cff2 100644 --- a/isis/src/base/objs/TableRecord/TableRecord.h +++ b/isis/src/base/objs/TableRecord/TableRecord.h @@ -38,6 +38,8 @@ namespace Isis { class TableRecord { public: TableRecord(); + TableRecord(std::string tableRecordStr, char fieldDelimiter, + std::vector fieldNames, int numOfFieldValues); ~TableRecord(); diff --git a/isis/src/control/apps/jigsaw/jigsaw.cpp b/isis/src/control/apps/jigsaw/jigsaw.cpp index 57f39a6370..ce9a4064b1 100644 --- a/isis/src/control/apps/jigsaw/jigsaw.cpp +++ b/isis/src/control/apps/jigsaw/jigsaw.cpp @@ -7,6 +7,11 @@ find files of those names at the top level of this repository. **/ /* SPDX-License-Identifier: CC0-1.0 */ #include +#include +#include +#include +#include +#include #include #include @@ -14,6 +19,7 @@ find files of those names at the top level of this repository. **/ #include #include + #include "Blob.h" #include "BundleAdjust.h" #include "BundleObservationSolveSettings.h" @@ -35,6 +41,8 @@ find files of those names at the top level of this repository. **/ #include "jigsaw.h" using namespace std; +using namespace HighFive; + namespace Isis { @@ -47,6 +55,77 @@ namespace Isis { void jigsaw(UserInterface &ui, Pvl *log) { + QString cubeList = ui.GetFileName("FROMLIST"); + + // Check for ADJUSTMENT_INPUT file and apply + try { + if (ui.WasEntered("ADJUSTMENT_INPUT")) { + + // Set default values + ui.Clear("TWIST"); + ui.PutBoolean("TWIST", false); + ui.PutBoolean("BUNDLEOUT_TXT", false); + ui.PutBoolean("IMAGESCSV", false); + ui.PutBoolean("OUTPUT_CSV", false); + ui.PutBoolean("RESIDUALS_CSV", false); + ui.PutBoolean("LIDAR_CSV", false); + ui.PutBoolean("OUTADJUSTMENTH5", false); + + File fileRead(ui.GetFileName("ADJUSTMENT_INPUT").toStdString(), File::ReadOnly); + SerialNumberList *snList = new SerialNumberList(cubeList); + + QString jigApplied = "JigApplied = " + Isis::iTime::CurrentLocalTime(); + + for (int i = 0; i < snList->size(); i++) { + Process p; + CubeAttributeInput inAtt; + Cube *c = p.SetInputCube(snList->fileName(i), inAtt, ReadWrite); + + if (c->hasBlob("CSMState", "String")) { + QString msg = "Unable to apply bundle adjustment values to cubes with CSMState blobs."; + throw IException(IException::User, msg, _FILEINFO_); + } + + QString serialNumber = snList->serialNumber(i); + QString cmatrixName = "InstrumentPointing"; + QString spvectorName = "InstrumentPosition"; + + std::string cmatrixKey = serialNumber.toStdString() + "/" + cmatrixName.toStdString(); + std::string spvectorKey = serialNumber.toStdString() + "/" + spvectorName.toStdString(); + + // Read h5 into table + DataSet datasetRead = fileRead.getDataSet(cmatrixKey); + auto cmatrixData = datasetRead.read(); + Table cmatrixTable(cmatrixName, cmatrixData, ','); + + datasetRead = fileRead.getDataSet(spvectorKey); + auto spvectorData = datasetRead.read(); + Table spvectorTable(spvectorName, spvectorData, ','); + + // Write bundle adjustment values out + cmatrixTable.Label().addComment(jigApplied); + c->write(cmatrixTable); + spvectorTable.Label().addComment(jigApplied); + c->write(spvectorTable); + + p.WriteHistory(*c); + } + + if (log) { + PvlGroup gp("JigsawResults"); + + gp += PvlKeyword("Status", "Bundle adjustment values from [" + ui.GetFileName("ADJUSTMENT_INPUT") + + "] were applied to the cubes in [" + cubeList+ "]"); + log->addLogGroup(gp); + } + + return; + } + } catch (IException &e) { + QString msg = "Unable to apply bundle adjustment values from [" + ui.GetFileName("ADJUSTMENT_INPUT") + "]"; + throw IException(e, IException::User, msg, _FILEINFO_); + } + // Check to make sure user entered something to adjust... Or can just points be in solution? // YES - we should be able to just TRIANGULATE the points in the control net // right now to do this we have to fake out jigsaw by @@ -59,7 +138,6 @@ namespace Isis { } QString cnetFile = ui.GetFileName("CNET"); - QString cubeList = ui.GetFileName("FROMLIST"); // retrieve settings from jigsaw gui BundleSettingsQsp settings = bundleSettings(ui); @@ -77,6 +155,7 @@ namespace Isis { } } settings->setCubeList(cubeList); + BundleAdjust *bundleAdjustment = NULL; try { // Get the held list if entered and prep for bundle adjustment @@ -130,24 +209,63 @@ namespace Isis { bundleSolution->outputResiduals(); } - // write lidar csv output file - if (ui.GetBoolean("LIDAR_CSV")) { - bundleSolution->outputLidarCSV(); - } + // write lidar csv output file + if (ui.GetBoolean("LIDAR_CSV")) { + bundleSolution->outputLidarCSV(); + } // write updated control net bundleAdjustment->controlNet()->Write(ui.GetFileName("ONET")); - // write updated lidar data file - if (ui.WasEntered("LIDARDATA")) { - if (ui.GetString("OLIDARFORMAT") == "JSON") { - bundleAdjustment->lidarData()->write(ui.GetFileName("OLIDARDATA"),LidarData::Format::Json); - } - else { - bundleAdjustment->lidarData()->write(ui.GetFileName("OLIDARDATA"),LidarData::Format::Binary); + // write updated lidar data file + if (ui.WasEntered("LIDARDATA")) { + if (ui.GetString("OLIDARFORMAT") == "JSON") { + bundleAdjustment->lidarData()->write(ui.GetFileName("OLIDARDATA"),LidarData::Format::Json); + } + else { + bundleAdjustment->lidarData()->write(ui.GetFileName("OLIDARDATA"),LidarData::Format::Binary); + } } - } + PvlGroup gp("JigsawResults"); + QString jigComment = "Jigged = " + Isis::iTime::CurrentLocalTime(); + + std::string outputFilePrefix = settings->outputFilePrefix().toStdString(); + + // ALWAYS* WRITE OUT ADJUSTMENT VALUES + // Do NOT write out for cubes w/ CSMState (TODO) + if (ui.GetBoolean("OUTADJUSTMENTH5")) { + std::string adjustmentFilename = outputFilePrefix + "adjustment_out.h5"; + + File file(adjustmentFilename, File::Truncate); + + for (int i = 0; i < bundleAdjustment->numberOfImages(); i++) { + Process p; + CubeAttributeInput inAtt; + Cube *c = p.SetInputCube(bundleAdjustment->fileName(i), inAtt, ReadWrite); + + // Only for ISIS adjustment values + if (!c->hasBlob("CSMState", "String")) { + Table cmatrix = bundleAdjustment->cMatrix(i); + Table spvector = bundleAdjustment->spVector(i); + + QString serialNumber = bundleAdjustment->serialNumberList()->serialNumber(i); + QString cmatrixName = cmatrix.Name(); + QString spvectorName = spvector.Name(); + + std::string cmatrixKey = serialNumber.toStdString() + "/" + cmatrixName.toStdString(); + std::string spvectorKey = serialNumber.toStdString() + "/" + spvectorName.toStdString(); + + // Save bundle adjustment values to HDF5 file + std::string cmatrixTableStr = Table::toString(cmatrix).toStdString(); + DataSet dataset = file.createDataSet(cmatrixKey, cmatrixTableStr); + std::string spvectorTableStr = Table::toString(spvector).toStdString(); + dataset = file.createDataSet(spvectorKey, spvectorTableStr); + } + } + file.flush(); + } + // Update the cube pointing if requested but ONLY if bundle has converged if (ui.GetBoolean("UPDATE") ) { if ( !bundleAdjustment->isConverged() ) { @@ -156,6 +274,8 @@ namespace Isis { throw IException(IException::Unknown, msg, _FILEINFO_); } else { + + // Loop through images for (int i = 0; i < bundleAdjustment->numberOfImages(); i++) { Process p; CubeAttributeInput inAtt; @@ -174,8 +294,7 @@ namespace Isis { break; } - // Update the image parameters - QString jigComment = "Jigged = " + Isis::iTime::CurrentLocalTime(); + // Only apply adjustment_input values for non-CSM for now if (c->hasBlob("CSMState", "String")) { Blob csmStateBlob("CSMState", "String"); // Read the BLOB from the cube to propagate things like the model @@ -185,8 +304,8 @@ namespace Isis { csmStateBlob.setData(modelState.c_str(), modelState.size()); csmStateBlob.Label().addComment(jigComment); c->write(csmStateBlob); - } - else { + } else { + // Write bundle adjustment values to cube Table cmatrix = bundleAdjustment->cMatrix(i); cmatrix.Label().addComment(jigComment); Table spvector = bundleAdjustment->spVector(i); @@ -194,6 +313,7 @@ namespace Isis { c->write(cmatrix); c->write(spvector); } + p.WriteHistory(*c); } gp += PvlKeyword("Status", "Camera pointing updated"); diff --git a/isis/src/control/apps/jigsaw/jigsaw.xml b/isis/src/control/apps/jigsaw/jigsaw.xml index 118c834b89..08abf03688 100644 --- a/isis/src/control/apps/jigsaw/jigsaw.xml +++ b/isis/src/control/apps/jigsaw/jigsaw.xml @@ -38,6 +38,19 @@ Updated camera information is only written to the cube labels if the bundle converges and the UPDATE parameter is selected.

+

+ In order to apply bundle values using ADJUSTMENT_INPUT, only the option FROMLIST + must be set. If the ADJUSTMENT_INPUT is set to an HDF5 file containing bundle adjustment values + (usually the HDF5 output file from OUTADJUSTMENTH5), those values can be applied to the + cube labels. The adjustment file entails a group of HDF5 datasets where each dataset + contains the instrument pointing and position for each cube. The dataset key is composed + of the cube serial number and the table name, either "InstrumentPointing" or + "InstrumentPosition", separated by a forward slash "/". For example: +

+
+      SerialNumber: MRO/CTX/1016816309:030
+      Dataset key for instrument pointing: MRO/CTX/1016816309:030/InstrumentPointing
+    

The input control net may be created by finding each image footprint with footprintinit, @@ -53,7 +66,10 @@ file with the IMAGESCSV option and likewise for the point statistics with the OUTPUT_CSV option. RESIDUALS_CSV provides a table of the measured image coordinates, the final sample, line, and overall residuals - of every control measure in both millimeters and pixels. + of every control measure in both millimeters and pixels. OUTADJUSTMENTH5 stores bundle + adjustment values in an HDF5 file. Currently, only ISIS adjustment values are saved to file so + if all the cubes in the FROMLIST contain a CSM state blob then the adjustment_out.h5 + file will be created but empty.

Observation Equations

@@ -711,6 +727,9 @@ autoseed or qnet. It contains the control points and associated measures. + + ADJUSTMENT_INPUT + *.net @@ -727,6 +746,9 @@ the final coordinates of the control points and residuals for each measurement. + + ADJUSTMENT_INPUT + *.net @@ -875,6 +897,9 @@ POINT_RADIUS_SIGMA + + ADJUSTMENT_INPUT + @@ -891,6 +916,9 @@ No + + ADJUSTMENT_INPUT + @@ -1283,6 +1311,7 @@ Yes + Fit polynomial over the existing pointing @@ -1769,7 +1798,7 @@ Output file prefix File prefix to prepend for the generated output files. Any prefix that is not a - file path will have an underscore placed between the prefix and file name. + file path will have an underscore placed between the prefix and file name. @@ -1835,6 +1864,91 @@ no + + + Outputs bundle adjustment values to HDF5 file - adjustment_out.h5 + + Selection of this parameter flags output of bundle adjustment + values to an HDF5 file. The HDF5 file may be empty if all the cubes + have CSM states. + + boolean + + yes + + + + + filename + none + input + + Reads in and applies adjustment values + + + Reads in and applies bundle adjustment values from an HDF5 file, + usually the outputted adjustment_out.h5 file. + + + *.h5 + + + ONET + CNET + LIDARDATA + OLIDARDATA + OLIDARFORMAT + SCCONFIG + OBSERVATIONS + RADIUS + UPDATE + OUTLIER_REJECTION + REJECTION_MULTIPLIER + ERRORPROPAGATION + MODEL1 + MAX_MODEL1_C_QUANTILE + MODEL2 + MAX_MODEL2_C_QUANTILE + MODEL3 + MAX_MODEL3_C_QUANTILE + SIGMA0 + MAXITS + CKDEGREE + CKSOLVEDEGREE + CAMSOLVE + + OVEREXISTING + SPKDEGREE + SPKSOLVEDEGREE + SPSOLVE + OVERHERMITE + CSMSOLVESET + CSMSOLVETYPE + CSMSOLVELIST + SOLVETARGETBODY + TBPARAMETERS + CONTROL_POINT_COORDINATE_TYPE_BUNDLE + CONTROL_POINT_COORDINATE_TYPE_REPORTS + POINT_LATITUDE_SIGMA + POINT_LONGITUDE_SIGMA + POINT_RADIUS_SIGMA + POINT_X_SIGMA + POINT_Y_SIGMA + POINT_Z_SIGMA + SPACECRAFT_POSITION_SIGMA + SPACECRAFT_VELOCITY_SIGMA + SPACECRAFT_ACCELERATION_SIGMA + CAMERA_ANGLES_SIGMA + CAMERA_ANGULAR_VELOCITY_SIGMA + CAMERA_ANGULAR_ACCELERATION_SIGMA + + + diff --git a/isis/tests/FunctionalTestsJigsaw.cpp b/isis/tests/FunctionalTestsJigsaw.cpp index 3abcc3cfe2..61d957a41c 100644 --- a/isis/tests/FunctionalTestsJigsaw.cpp +++ b/isis/tests/FunctionalTestsJigsaw.cpp @@ -15,6 +15,11 @@ #include "CSMCamera.h" #include "LidarData.h" #include "SerialNumber.h" +#include "BundleAdjust.h" +#include "BundleSettings.h" +#include +#include + #include "jigsaw.h" @@ -1867,3 +1872,47 @@ TEST_F(LidarNetwork, FunctionalTestJigsawLidar) { } } + +TEST_F(ApolloNetwork, FunctionalTestJigsawSaveApplyValues) { + QVector args = {"spsolve=position", + "update=yes", + "bundleout_txt=no", + "cnet="+controlNetPath, + "fromlist="+tempDir.path() + "/cubes.lis", + "onet="+tempDir.path()+"/apollo_out.net", + "file_prefix="+tempDir.path()+"/"}; + + UserInterface ui(APP_XML, args); + + jigsaw(ui); + + // Check apollo_jigsaw.h5 was created + QString bundleOutput = tempDir.path()+"/adjustment_out.h5"; + HighFive::File file(bundleOutput.toStdString(), HighFive::File::ReadWrite); + + std::string datasetName = "/APOLLO15/METRIC/1971-08-01T15:37:39.428"; + QString cmatrixName = "InstrumentPointing"; + QString spvectorName = "InstrumentPosition"; + std::string cmatrixKey = datasetName + "/" + cmatrixName.toStdString(); + std::string spvectorKey = datasetName + "/" + spvectorName.toStdString(); + + HighFive::DataSet datasetRead = file.getDataSet(cmatrixKey); + auto cmatrixData = datasetRead.read(); + Table cmatrixTable(cmatrixName, cmatrixData, ','); + std::string cmatrixTableStr = Table::toString(cmatrixTable).toStdString(); + + datasetRead = file.getDataSet(spvectorKey); + auto spvectorData = datasetRead.read(); + Table spvectorTable(spvectorName, spvectorData, ','); + std::string spvectorTableStr = Table::toString(spvectorTable).toStdString(); + + EXPECT_EQ(cmatrixTable.RecordFields(), 8); + EXPECT_EQ(spvectorTable.RecordFields(), 7); + + EXPECT_EQ(cmatrixTableStr, + "J2000Q0,J2000Q1,J2000Q2,J2000Q3,AV1,AV2,AV3,ET\n0.72889620121855,0.66172757646101,-0.1261913882606,0.12207651669777,4.29360266307594e-04,6.9419874212449e-04,-6.23609851587137e-04,-896818899.38874\n"); + EXPECT_EQ(spvectorTableStr, + "J2000X,J2000Y,J2000Z,J2000XV,J2000YV,J2000ZV,ET\n491.19844009026,1198.1045282857,1313.7703671439,1.5198029518433,-0.58925196165899,-0.046463883259045,-896818899.38874\n"); + + file.flush(); +} diff --git a/isis/tests/TableTests.cpp b/isis/tests/TableTests.cpp index b8da823ec9..ee00d25515 100644 --- a/isis/tests/TableTests.cpp +++ b/isis/tests/TableTests.cpp @@ -295,3 +295,33 @@ TEST(TableTests, Clear) { EXPECT_EQ(t.Records(), 0); } + +TEST(TableTests, FromString) { + std::string tableStr = "J2000Ang1,J2000Ang2,J2000Ang3\n" + "-1.0261086365746,1.3843980236775,0.97666760713915\n" + "-0.026127047776247,0.034245411189199,0.0052635095732964\n" + "-0.005717949450684,-0.0039014897927048,2.3750859084069e-05\n" + "260093852.48957,46.12915199995,2.0\n"; + + std::cout << "tableStr=" << tableStr << std::endl; + + QString tableName = QString::fromStdString("TestTableName"); + + std::stringstream tableStrStream; + tableStrStream << tableStr; + + Table table(tableName, tableStr, ','); + std::cout << "Created table with table string" << std::endl; + std::cout << "Table name=" << table.Name().toStdString() << std::endl; + std::cout << "Table recordFields=" << static_cast(table.RecordFields()) << std::endl; + std::cout << "Table recordSize=" << static_cast(table.RecordSize()) << std::endl; + + for (int i = 0; i < table.Records(); i++) { + std::cout << "Table[" << i << "] record=" << TableRecord::toString(table[i]).toStdString() << std::endl; + } + + QString tableToString = Table::toString(table); + std::cout << "tableToString=" << tableToString.toStdString() << std::endl; + + EXPECT_EQ(tableStr, tableToString.toStdString()); +}