Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create file splitting object for GeoJSON work. #5470

Merged
merged 2 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions hoot-core-test/src/test/cpp/hoot/core/io/MultiFileWriterTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
* This file is part of Hootenanny.
*
* Hootenanny is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --------------------------------------------------------------------
*
* The following copyright notices are generated automatically. If you
* have a new notice to add, please use the format:
* " * @copyright Copyright ..."
* This will properly maintain the copyright information. Maxar
* copyrights will be updated automatically.
*
* @copyright Copyright (C) 2022 Maxar (http://www.maxar.com/)
*/

// Hoot
#include <hoot/core/TestUtils.h>
#include <hoot/core/io/MultiFileWriter.h>

namespace hoot
{

class MultiFileWriterTest : public HootTestFixture
{
CPPUNIT_TEST_SUITE(MultiFileWriterTest);
CPPUNIT_TEST(runBufferTest);
CPPUNIT_TEST(runSingleFileTest);
CPPUNIT_TEST(runGeometryTest);
CPPUNIT_TEST(runFCodeTest);
CPPUNIT_TEST_SUITE_END();

public:

MultiFileWriterTest()
: HootTestFixture("test-files/io/MultiFileWriter/",
"test-output/io/MultiFileWriter/")
{
}

void runBufferTest()
{
// Indices don't matter for the buffer test
std::vector<QString> indices({""});
// Create the single buffer object
MultiFileWriter writer(MultiFileWriter::MultiFileWriterType::SingleBuffer);
// Open
writer.open();
// Write all the testing data to the buffer
writeData(writer, indices);
QString value = writer.getBuffer();
// Close the writer
writer.close();
// Test the content
HOOT_STR_EQUALS(FileUtils::readFully(_inputPath + "SingleFileTest.json"), value);
}

void runSingleFileTest()
{
QString base = "SingleFileTest";
QString extension = ".json";
// Indices don't matter for the single file test
std::vector<QString> indices({""});
// Run the single file object test
runTest(base, extension, indices, MultiFileWriter::SingleFile);
}

void runGeometryTest()
{
QString base = "GeometryTest";
QString extension = ".json";
// Split the output into three different files
std::vector<QString> indices({MultiFileWriter::POINTS, MultiFileWriter::LINES, MultiFileWriter::POLYGONS});
// Run multi-file geometry test
runTest(base, extension, indices, MultiFileWriter::MultiGeom);
}

void runFCodeTest()
{
QString base = "FCodeTest";
QString extension = ".json";
// Split the output into 12 files
std::vector<QString> indices({"AAL200", "ABA030", "ABA040", "ABH080", "ABH090", "ABH140", "ABH160", "ADA010", "AEA010", "AEB010", "AEB020", "AEC030"});
// Run the multi-file F-Code test
runTest(base, extension, indices, MultiFileWriter::MultiFCode);
}

void runTest(const QString& base, const QString& ext, const std::vector<QString>& indices, MultiFileWriter::MultiFileWriterType type)
{
// Create the multi-file object
MultiFileWriter writer(type);
// Open the file
writer.open(QString("%1%2%3").arg(_outputPath, base, ext));
// Write all the testing data to the object
writeData(writer, indices);
// Close the writer
writer.close();
// Test the output
checkFiles(base, ext, indices);
}

void writeData(MultiFileWriter& writer, const std::vector<QString>& indices)
{
int total_elements = 24;
int elements_per_file = total_elements / static_cast<int>(indices.size());
int index = 0;
// Write a bunch of things to the header
writer.writeHeader("{");
writer.writeHeader("\"file\": \"This is the header\",\n");
writer.writeHeader("\"contents\": [\n");
// Iterate a total number of elements
for (int i = 0; i < total_elements; ++i)
{
// Update the index
index = i / elements_per_file;
writer.setCurrentFileIndex(indices[index]);
// Write the content
writer.write("{ \"element-id\": \"");
writer.write(QString::number(i));
writer.write("\", \"element-type\": \"");
writer.write(indices[index]);
writer.write("\" }");
if (i % elements_per_file != elements_per_file - 1)
writer.write(",");
writer.write("\n");
}
// Write the footer
writer.writeFooter("]");
writer.writeFooter("}\n");
}

void checkFiles(const QString& base, const QString& ext, const std::vector<QString>& indices)
{
// Test the output
for (const auto& index : indices)
{
QString dash = (index.isEmpty() ? "" : "-");
HOOT_FILE_EQUALS(QString("%1%2%3%4%5").arg(_inputPath, base, dash, index, ext),
QString("%1%2%3%4%5").arg(_outputPath, base, dash, index, ext));
}
}

};

CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(MultiFileWriterTest, "quick");

}
198 changes: 198 additions & 0 deletions hoot-core/src/main/cpp/hoot/core/io/MultiFileWriter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/*
* This file is part of Hootenanny.
*
* Hootenanny is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --------------------------------------------------------------------
*
* The following copyright notices are generated automatically. If you
* have a new notice to add, please use the format:
* " * @copyright Copyright ..."
* This will properly maintain the copyright information. Maxar
* copyrights will be updated automatically.
*
* @copyright Copyright (C) 2022 Maxar (http://www.maxar.com/)
*/
#include "MultiFileWriter.h"

namespace hoot
{

const QString MultiFileWriter::POINTS = "Points";
const QString MultiFileWriter::LINES = "Lines";
const QString MultiFileWriter::POLYGONS = "Polygons";

MultiFileWriter::MultiFileWriter(MultiFileWriterType type)
: _type(type),
_currentDevice(nullptr),
_header(&_fileHeader),
_footer(&_fileFooter)
{
_header.setCodec("UTF-8");
_footer.setCodec("UTF-8");
}

MultiFileWriter::~MultiFileWriter()
{
// Close the file(s)/buffer
close();
}

void MultiFileWriter::setCurrentFileIndex(const QString& index)
{
switch(_type)
{
case MultiFileWriterType::SingleBuffer:
case MultiFileWriterType::SingleFile:
// Nothing needs to be done here, ignore the index
break;
default:
// Only do something if the index is different
if (_currentIndex != index)
{
// For multigeometry output check for `Points`, `Lines`, and `Polygons`
if (_type == MultiFileWriterType::MultiGeom && index != POINTS && index != LINES && index != POLYGONS)
throw HootException(QString("Illegal file index in multigeometry mode."));
// Reset the current device
_currentDevice = nullptr;
// Set the index
_currentIndex = index;
}
break;
}
}

void MultiFileWriter::write(const QString& contents, MultiFileWriterSection section)
{
// Check the section
if (section == MultiFileWriterSection::SectionBody)
{
// Check the current device before writing
if (!_currentDevice)
{
// Check the type
if (_type == MultiFileWriterType::SingleFile)
{
// Create the single file object
_files.push_back(std::make_shared<QFile>(_filePath));
_currentDevice = _files[0].get();
_deviceMap.emplace("SingleFile", _currentDevice);
// Open up the single file object
if (!_currentDevice->open(QIODevice::WriteOnly | QIODevice::Text))
throw HootException(QString("Error opening %1 for writing").arg(_filePath));
// Write the header to the new file before writing what was requested
_currentDevice->write(_fileHeader.toUtf8());
}
else if (_type == MultiFileWriterType::SingleBuffer)
{
if (_stringBuffer.isOpen())
throw HootException(QString("Cannot write to string-buffered object before calling open()."));
// Open the buffer and set it to be the current device
_stringBuffer.open(QBuffer::WriteOnly);
_currentDevice = &_stringBuffer;
// Write the header to the new file before writing what was requested
_currentDevice->write(_fileHeader.toUtf8());
}
else
{
if (_currentIndex.isEmpty())
throw HootException(QString("Cannot write to multifile output without calling setCurrentFileIndex() with non-empty value."));
// Check if the current index has an open file object
if (_deviceMap.find(_currentIndex) != _deviceMap.end())
_currentDevice = _deviceMap[_currentIndex];
else
{
// Open a new device and add it to the map
QString filename = _filePath.arg(_currentIndex);
// Create the file object, push it to the back
_files.push_back(std::make_shared<QFile>(filename));
_currentDevice = _files.back().get();
_deviceMap.emplace(_currentIndex, _currentDevice);
// Open up the single file object
if (!_currentDevice->open(QIODevice::WriteOnly | QIODevice::Text))
throw HootException(QString("Error opening %1 for writing").arg(filename));
// Write the header to the new file before writing what was requested
_currentDevice->write(_fileHeader.toUtf8());
}
}
}
_currentDevice->write(contents.toUtf8());
}
else if (section == MultiFileWriterSection::SectionHeader)
_header << contents;
else if (section == MultiFileWriterSection::SectionFooter)
_footer << contents;
}

QString MultiFileWriter::getBuffer() const
{
if (_type != MultiFileWriterType::SingleBuffer)
throw HootException(QString("Cannot call getBuffer() on non-string-buffered output objects."));
// Output the object content plus the footer
return QString::fromUtf8(_stringBuffer.buffer()) + _fileFooter;
}

void MultiFileWriter::open()
{
// This version of open can only be called on SingleBuffer objects
if (_type != MultiFileWriterType::SingleBuffer)
throw HootException(QString("Cannot call open() without filename on non-string-buffered output objects."));
// Cannot re-open the file once it has been written to
if (_stringBuffer.isOpen())
throw HootException(QString("Cannot call open() on object that is already open."));
}

void MultiFileWriter::open(const QString& filename)
{
// This version of open() cannot be used with the SingleBuffer object
if (_type == MultiFileWriterType::SingleBuffer)
throw HootException(QString("Cannot call open(<filename>) on string-buffered output objects."));
else if (_type == MultiFileWriterType::SingleFile)
_filePath = filename;
else
{
_filePath = filename;
// MultiFile writer will update the filename in QString::arg() format so files can be created
QFileInfo fi(_filePath);
QString extension = fi.suffix();
// Update the filename
_filePath.insert(_filePath.length() - (extension.length() + 1), "-%1");
// Don't open anything until something is actually written out the the file
}
}

void MultiFileWriter::close()
{
switch(_type)
{
case MultiFileWriterType::SingleBuffer:
// Only need to close the string buffer
_stringBuffer.close();
break;
default:
// Iterate all open devices
for (auto device = _deviceMap.cbegin(); device != _deviceMap.cend(); ++device)
{
if (device->second->isOpen())
{
// Any device that is open, has had something written to it, write the footer
device->second->write(_fileFooter.toUtf8());
// Then close it
device->second->close();
}
}
}
}

}
Loading