/***************************************************************************************** * * * OpenSpace * * * * Copyright (c) 2014-2021 * * * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * * software and associated documentation files (the "Software"), to deal in the Software * * without restriction, including without limitation the rights to use, copy, modify, * * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * * permit persons to whom the Software is furnished to do so, subject to the following * * conditions: * * * * The above copyright notice and this permission notice shall be included in all copies * * or substantial portions of the Software. * * * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, * * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A * * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF * * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE * * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * ****************************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr const int8_t DataCacheFileVersion = 10; constexpr const int8_t LabelCacheFileVersion = 10; constexpr const int8_t ColorCacheFileVersion = 10; bool startsWith(std::string lhs, std::string_view rhs) noexcept { for (size_t i = 0; i < lhs.size(); i++) { lhs[i] = static_cast(tolower(lhs[i])); } return (rhs.size() <= lhs.size()) && (lhs.substr(0, rhs.size()) == rhs); } void strip(std::string& line) noexcept { // 1. Remove all spaces from the beginning // 2. Remove # // 3. Remove all spaces from the new beginning // 4. Remove all spaces from the end while (!line.empty() && (line[0] == ' ' || line[0] == '\t')) { line = line.substr(1); } if (!line.empty() && line[0] == '#') { line = line.substr(1); } while (!line.empty() && (line[0] == ' ' || line[0] == '\t')) { line = line.substr(1); } while (!line.empty() && (line.back() == ' ' || line.back() == '\t')) { line = line.substr(0, line.size() - 1); } } template void checkSize(U value, std::string_view message) { if (value > std::numeric_limits::max()) { throw ghoul::RuntimeError(fmt::format("Error saving file: {}", message)); } } template using LoadCacheFunc = std::function(std::filesystem::path)>; template using SaveCacheFunc = std::function; template using LoadSpeckFunc = std::function; template T internalLoadFileWithCache(std::filesystem::path speckPath, openspace::speck::SkipAllZeroLines skipAllZeroLines, LoadSpeckFunc loadSpeckFunction, LoadCacheFunc loadCacheFunction, SaveCacheFunc saveCacheFunction) { static_assert( std::is_same_v || std::is_same_v || std::is_same_v ); std::filesystem::path cached = FileSys.cacheManager()->cachedFilename(speckPath); if (std::filesystem::exists(cached)) { LINFOC( "SpeckLoader", fmt::format("Cached file {} used for file {}", cached, speckPath) ); std::optional dataset = loadCacheFunction(cached); if (dataset.has_value()) { // We could load the cache file and we are now done with this return *dataset; } else { FileSys.cacheManager()->removeCacheFile(cached); } } LINFOC("SpeckLoader", fmt::format("Loading file {}", speckPath)); T dataset = loadSpeckFunction(speckPath, skipAllZeroLines); if (!dataset.entries.empty()) { LINFOC("SpeckLoader", "Saving cache"); saveCacheFunction(dataset, cached); } return dataset; } } // namespace namespace openspace::speck { namespace data { Dataset loadFile(std::filesystem::path path, SkipAllZeroLines skipAllZeroLines) { ghoul_assert(std::filesystem::exists(path), "File must exist"); std::ifstream file(path); if (!file.good()) { throw ghoul::RuntimeError(fmt::format("Failed to open speck file {}", path)); } Dataset res; int nDataValues = 0; std::string line; // First phase: Loading the header information while (std::getline(file, line)) { // Ignore empty line or commented-out lines if (line.empty() || line[0] == '#') { continue; } // Guard against wrong line endings (copying files from Windows to Mac) causes // lines to have a final \r if (line.back() == '\r') { line = line.substr(0, line.length() - 1); } strip(line); // If the first character is a digit, we have left the preamble and are in the // data section of the file if (std::isdigit(line[0]) || line[0] == '-') { break; } if (startsWith(line, "datavar")) { // each datavar line is following the form: // datavar // with being the index of the data variable std::stringstream str(line); std::string dummy; Dataset::Variable v; str >> dummy >> v.index >> v.name; nDataValues += 1; res.variables.push_back(v); continue; } if (startsWith(line, "texturevar")) { // each texturevar line is following the form: // texturevar // where is the data value index where the texture index is stored if (res.textureDataIndex != -1) { throw ghoul::RuntimeError(fmt::format( "Error loading speck file {}: Texturevar defined twice", path )); } std::stringstream str(line); std::string dummy; str >> dummy >> res.textureDataIndex; continue; } if (startsWith(line, "polyorivar")) { // each polyorivar line is following the form: // texturevar // where is the data value index where the orientation index storage // starts. There are 6 values stored in total, xyz + uvw if (res.orientationDataIndex != -1) { throw ghoul::RuntimeError(fmt::format( "Error loading speck file {}: Orientation index defined twice", path )); } std::stringstream str(line); std::string dummy; str >> dummy >> res.orientationDataIndex; // Ok.. this is kind of weird. Speck unfortunately doesn't tell us in the // specification how many values a datavar has. Usually this is 1 value per // datavar, unless it is a polygon orientation thing. Now, the datavar name // for these can be anything (have seen 'orientation' and 'ori' before, so we // can't really check by name for these or we will miss some if they are // mispelled or whatever. So we have to go the roundabout way of adding the // 5 remaining values (the 6th nDataValue was already added in the // corresponding 'datavar' section) here nDataValues += 5; continue; } if (startsWith(line, "texture")) { // each texture line is following one of two forms: // 1: texture -M 1 halo.sgi // 2: texture 1 M1.sgi // The parameter in #1 is currently being ignored std::stringstream str(line); std::string dummy; str >> dummy; if (line.find('-') != std::string::npos) { str >> dummy; } Dataset::Texture texture; str >> texture.index >> texture.file; for (const Dataset::Texture& t : res.textures) { if (t.index == texture.index) { throw ghoul::RuntimeError(fmt::format( "Error loading speck file {}: Texture index '{}' defined twice", path, texture.index )); } } res.textures.push_back(texture); continue; } } std::sort( res.variables.begin(), res.variables.end(), [](const Dataset::Variable& lhs, const Dataset::Variable& rhs) { return lhs.index < rhs.index; } ); std::sort( res.textures.begin(), res.textures.end(), [](const Dataset::Texture& lhs, const Dataset::Texture& rhs) { return lhs.index < rhs.index; } ); // For the first line, we already loaded it and rejected it above, so if we do another // std::getline, we'd miss the first data value line bool isFirst = true; while (isFirst || std::getline(file, line)) { isFirst = false; // Ignore empty line or commented-out lines if (line.empty() || line[0] == '#') { continue; } // Guard against wrong line endings (copying files from Windows to Mac) causes // lines to have a final \r if (line.back() == '\r') { line = line.substr(0, line.length() - 1); } strip(line); if (line.empty()) { continue; } // If the first character is a digit, we have left the preamble and are in the // data section of the file if (!std::isdigit(line[0]) && line[0] != '-') { throw ghoul::RuntimeError(fmt::format( "Error loading speck file {}: Header information and datasegment " "intermixed", path )); } bool allZero = true; std::stringstream str(line); Dataset::Entry entry; str >> entry.position.x >> entry.position.y >> entry.position.z; allZero &= (entry.position == glm::vec3(0.0)); entry.data.resize(nDataValues); for (int i = 0; i < nDataValues; i += 1) { str >> entry.data[i]; allZero &= (entry.data[i] == 0.0); } if (skipAllZeroLines && allZero) { continue; } std::string rest; std::getline(str, rest); if (!rest.empty()) { strip(rest); entry.comment = rest; } res.entries.push_back(std::move(entry)); } #ifdef _DEBUG if (!res.entries.empty()) { size_t nValues = res.entries[0].data.size(); ghoul_assert(nDataValues == nValues, "nDataValues calculation went wrong"); for (const Dataset::Entry& e : res.entries) { ghoul_assert( e.data.size() == nDataValues, "Row had different number of data values" ); } } #endif return res; } std::optional loadCachedFile(std::filesystem::path path) { std::ifstream file(path, std::ios::binary); if (!file.good()) { return std::nullopt; } Dataset result; int8_t fileVersion; file.read(reinterpret_cast(&fileVersion), sizeof(int8_t)); if (fileVersion != DataCacheFileVersion) { // Incompatible version and we won't be able to read the file return std::nullopt; } // // Read variables uint16_t nVariables; file.read(reinterpret_cast(&nVariables), sizeof(uint16_t)); result.variables.resize(nVariables); for (int i = 0; i < nVariables; i += 1) { Dataset::Variable var; int16_t idx; file.read(reinterpret_cast(&idx), sizeof(int16_t)); var.index = idx; uint16_t len; file.read(reinterpret_cast(&len), sizeof(uint16_t)); var.name.resize(len); file.read(var.name.data(), len); result.variables[i] = std::move(var); } // // Read textures uint16_t nTextures; file.read(reinterpret_cast(&nTextures), sizeof(uint16_t)); result.textures.resize(nTextures); for (int i = 0; i < nTextures; i += 1) { Dataset::Texture tex; int16_t idx; file.read(reinterpret_cast(&idx), sizeof(int16_t)); tex.index = idx; uint16_t len; file.read(reinterpret_cast(&len), sizeof(uint16_t)); tex.file.resize(len); file.read(tex.file.data(), len); result.textures[i] = std::move(tex); } // // Read indices int16_t texDataIdx; file.read(reinterpret_cast(&texDataIdx), sizeof(int16_t)); result.textureDataIndex = texDataIdx; int16_t oriDataIdx; file.read(reinterpret_cast(&oriDataIdx), sizeof(int16_t)); result.orientationDataIndex = oriDataIdx; // // Read entries uint64_t nEntries; file.read(reinterpret_cast(&nEntries), sizeof(uint64_t)); result.entries.reserve(nEntries); for (uint64_t i = 0; i < nEntries; i += 1) { Dataset::Entry e; file.read(reinterpret_cast(&e.position.x), sizeof(float)); file.read(reinterpret_cast(&e.position.y), sizeof(float)); file.read(reinterpret_cast(&e.position.z), sizeof(float)); uint16_t nValues; file.read(reinterpret_cast(&nValues), sizeof(uint16_t)); e.data.resize(nValues); file.read(reinterpret_cast(e.data.data()), nValues * sizeof(float)); uint16_t len; file.read(reinterpret_cast(&len), sizeof(uint16_t)); if (len > 0) { std::string comment; comment.resize(len); file.read(comment.data(), len); e.comment = std::move(comment); } result.entries.push_back(std::move(e)); } return result; } void saveCachedFile(const Dataset& dataset, std::filesystem::path path) { std::ofstream file(path, std::ofstream::binary); file.write(reinterpret_cast(&DataCacheFileVersion), sizeof(int8_t)); // // Store variables checkSize(dataset.variables.size(), "Too many variables"); uint16_t nVariables = static_cast(dataset.variables.size()); file.write(reinterpret_cast(&nVariables), sizeof(uint16_t)); for (const Dataset::Variable& var : dataset.variables) { checkSize(var.index, "Variable index too large"); int16_t idx = static_cast(var.index); file.write(reinterpret_cast(&idx), sizeof(int16_t)); checkSize(var.name.size(), "Variable name too long"); uint16_t len = static_cast(var.name.size()); file.write(reinterpret_cast(&len), sizeof(uint16_t)); file.write(var.name.data(), len); } // // Store textures checkSize(dataset.textures.size(), "Too many textures"); uint16_t nTextures = static_cast(dataset.textures.size()); file.write(reinterpret_cast(&nTextures), sizeof(uint16_t)); for (const Dataset::Texture& tex : dataset.textures) { checkSize(tex.index, "Texture index too large"); int16_t idx = static_cast(tex.index); file.write(reinterpret_cast(&idx), sizeof(int16_t)); checkSize(tex.file.size(), "Texture file too long"); uint16_t len = static_cast(tex.file.size()); file.write(reinterpret_cast(&len), sizeof(uint16_t)); file.write(tex.file.data(), len); } // // Store indices checkSize(dataset.textureDataIndex, "Texture index too large"); int16_t texIdx = static_cast(dataset.textureDataIndex); file.write(reinterpret_cast(&texIdx), sizeof(int16_t)); checkSize(dataset.orientationDataIndex, "Orientation index too large"); int16_t orientationIdx = static_cast(dataset.orientationDataIndex); file.write(reinterpret_cast(&orientationIdx), sizeof(int16_t)); // // Store entries checkSize(dataset.entries.size(), "Too many entries"); uint64_t nEntries = static_cast(dataset.entries.size()); file.write(reinterpret_cast(&nEntries), sizeof(uint64_t)); for (const Dataset::Entry& e : dataset.entries) { file.write(reinterpret_cast(&e.position.x), sizeof(float)); file.write(reinterpret_cast(&e.position.y), sizeof(float)); file.write(reinterpret_cast(&e.position.z), sizeof(float)); checkSize(e.data.size(), "Too many data variables"); uint16_t nValues = static_cast(e.data.size()); file.write(reinterpret_cast(&nValues), sizeof(uint16_t)); file.write( reinterpret_cast(e.data.data()), e.data.size() * sizeof(float) ); if (e.comment.has_value()) { checkSize(e.comment->size(), "Comment too long"); } uint16_t commentLen = e.comment.has_value() ? static_cast(e.comment->size()) : 0; file.write(reinterpret_cast(&commentLen), sizeof(uint16_t)); if (e.comment.has_value()) { file.write(e.comment->data(), e.comment->size()); } } } Dataset loadFileWithCache(std::filesystem::path speckPath, SkipAllZeroLines skipAllZeroLines) { return internalLoadFileWithCache( speckPath, skipAllZeroLines, &loadFile, &loadCachedFile, &saveCachedFile ); } } // namespace data namespace label { Labelset loadFile(std::filesystem::path path, SkipAllZeroLines) { ghoul_assert(std::filesystem::exists(path), "File must exist"); std::ifstream file(path); if (!file.good()) { throw ghoul::RuntimeError(fmt::format("Failed to open speck file {}", path)); } Labelset res; std::string line; // First phase: Loading the header information while (std::getline(file, line)) { // Ignore empty line or commented-out lines if (line.empty() || line[0] == '#') { continue; } // Guard against wrong line endings (copying files from Windows to Mac) causes // lines to have a final \r if (line.back() == '\r') { line = line.substr(0, line.length() - 1); } strip(line); // If the first character is a digit, we have left the preamble and are in the // data section of the file if (std::isdigit(line[0]) || line[0] == '-') { break; } if (startsWith(line, "textcolor")) { // each textcolor line is following the form: // textcolor // with being the index of the color into some configuration file (not // really sure how these configuration files work, but they don't seem to be // included in the speck file) if (res.textColorIndex != -1) { throw ghoul::RuntimeError(fmt::format( "Error loading label file {}: Textcolor defined twice", path )); } std::stringstream str(line); std::string dummy; str >> dummy >> res.textColorIndex; continue; } } // For the first line, we already loaded it and rejected it above, so if we do another // std::getline, we'd miss the first data value line bool isFirst = true; while (isFirst || std::getline(file, line)) { isFirst = false; // Ignore empty line or commented-out lines if (line.empty() || line[0] == '#') { continue; } // Guard against wrong line endings (copying files from Windows to Mac) causes // lines to have a final \r if (line.back() == '\r') { line = line.substr(0, line.length() - 1); } strip(line); if (line.empty()) { continue; } // If the first character is a digit, we have left the preamble and are in the // data section of the file if (!std::isdigit(line[0]) && line[0] != '-') { throw ghoul::RuntimeError(fmt::format( "Error loading label file {}: Header information and datasegment " "intermixed", path )); } // Each line looks like this: // text