/***************************************************************************************** * * * 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 #include #include // @TODO (abock, 2020-08-07) All of the time handling in this class should be cleaned up // a bit. There are lots of conversions between ISO strings for time and Time // objects and back which eat up performance. For example, the TimeRange should // operate on Time objects rather than date strings and the DateTime likewise (if // this class needs to exist at all) namespace openspace::globebrowsing { namespace { // returns the number of days in a given month and year (takes leap year into account) constexpr int monthSize(int month, int year) { date::year_month_day_last d = date::year(year) / date::month(month) / date::last; return static_cast(static_cast(d.day())); } /** * singleIncrement is used for any of the date/time types, and handles overflow * values using the min/max parameters * * \param oper the date/time variable to operate on (will be changed) * \param val the value of the increment, which may be changed in this function * if an overflow occurs * \param min the minimum allowable value * \param max the maximum allowable value (determines where overflow occurs) */ bool singleIncrement(int& oper, int& val, int min, int max) { oper += val; if (oper <= max) { return true; } oper = oper - max - (1 - min); // Only single increments for the less-significant units on rollover val = 1; return false; } /** * singleDecrement is used for any of the date/time types, and handles underflow * values using the min/max parameters * * \param oper the date/time variable to operate on (will be changed) * \param val the value of the decrement, which may be changed in this function * if an underflow occurs * \param min the minimum allowable value * \param max the maximum allowable value (determines where underflow occurs) */ bool singleDecrement(int& oper, int& val, int min, int max) { oper -= val; if (oper >= min) { return true; } oper = oper + max + (1 - min); // Only single increments for the less-significant units on rollover val = 1; return false; } } // namespace RangedTime::RangedTime(std::string start, std::string end) : _start(std::move(start)) , _end(std::move(end)) { setStart(_start); setEnd(_end); } void RangedTime::setStart(std::string start) { Time t1; t1.setTime(start); _startJ2000 = t1.j2000Seconds(); _start = std::move(start); } void RangedTime::setEnd(std::string end) { Time t2; t2.setTime(end); _endJ2000 = t2.j2000Seconds(); _end = std::move(end); } bool RangedTime::includes(const Time& checkTime) const { const double tj = checkTime.j2000Seconds(); return (_startJ2000 <= tj && tj <= _endJ2000); } const char* RangedTime::clamp(const char* checkTime) { Time t; t.setTime(checkTime); const double tj = t.j2000Seconds(); if (tj < _startJ2000) { return _start.c_str(); } else if (tj > _endJ2000) { return _end.c_str(); } else { return checkTime; } } std::string_view RangedTime::start() const { return _start; } std::string_view RangedTime::end() const { return _end; } DateTime::DateTime(std::string_view initDateTime) { setTime(initDateTime); } void DateTime::setTime(std::string_view input) { // Indices into an ISO8601 YYYY-MM-ddTHH:mm:ss string constexpr const size_t IndexYear = 0; constexpr const size_t IndexMonth = 5; constexpr const size_t IndexDay = 8; constexpr const size_t IndexHour = 11; constexpr const size_t IndexMinute = 14; constexpr const size_t IndexSecond = 17; std::from_chars(input.data() + IndexYear, input.data() + IndexYear + 4, _year); std::from_chars(input.data() + IndexMonth, input.data() + IndexMonth + 2, _month); std::from_chars(input.data() + IndexDay, input.data() + IndexDay + 2, _day); std::from_chars(input.data() + IndexHour, input.data() + IndexHour + 2, _hour); std::from_chars(input.data() + IndexMinute, input.data() + IndexMinute + 2, _minute); std::from_chars(input.data() + IndexSecond, input.data() + IndexSecond + 2, _second); } std::string DateTime::ISO8601() const { return fmt::format( "{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}", _year, _month, _day, _hour, _minute, _second ); } double DateTime::J2000() const { char Buffer[20]; std::memset(Buffer, 0, 20); fmt::format_to( Buffer, "{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}", _year, _month, _day, _hour, _minute, _second ); return Time::convertTime(Buffer); } int DateTime::increment(int value, char unit, double error, double resolution) { unsigned int nIncrements = std::abs(static_cast(error / resolution)); if (nIncrements == 0) { nIncrements = 1; } for (unsigned int i = 0; i < nIncrements; ++i) { incrementOnce(value, unit); } return nIncrements; } void DateTime::incrementOnce(int value, char unit) { bool inBounds = true; switch (unit) { case 'm': if (singleIncrement(_minute, value, 0, 59)) { break; } [[ fallthrough ]]; case 'h': if (singleIncrement(_hour, value, 0, 23)) { break; } [[ fallthrough ]]; case 'd': if (singleIncrement(_day, value, 1, monthSize(_month, _year))) { break; } [[ fallthrough ]]; case 'M': inBounds = singleIncrement(_month, value, 1, 12); _day = std::clamp(_day, 1, monthSize(_month, _year)); if (inBounds) { break; } [[ fallthrough ]]; case 'y': _year += value; break; default: throw ghoul::RuntimeError(fmt::format( "Invalid unit in incrementOnce '{}'. Expected 'y', 'M', 'd', 'h', or 'm'", unit )); } } int DateTime::decrement(int value, char unit, double error, double resolution) { unsigned int nDecrements = std::abs(static_cast(error / resolution)); if (nDecrements == 0) { nDecrements = 1; } for (unsigned int i = 0; i < nDecrements; ++i) { decrementOnce(value, unit); } return nDecrements; } void DateTime::decrementOnce(int value, char unit) { bool inBounds = true; switch (unit) { case 'm': if (singleDecrement(_minute, value, 0, 59)) { break; } [[ fallthrough ]]; case 'h': if (singleDecrement(_hour, value, 0, 23)) { break; } [[ fallthrough ]]; case 'd': if (singleDecrement(_day, value, 1, monthSize(_month == 1 ? 12 : _month - 1, _year))) { break; } [[ fallthrough ]]; case 'M': inBounds = singleDecrement(_month, value, 1, 12); _day = std::clamp(_day, 1, monthSize(_month, _year)); if (inBounds) { break; } [[ fallthrough ]]; case 'y': _year -= value; break; default: throw ghoul::RuntimeError( "Invalid unit format in TQ decrementOnce '" + std::to_string(unit) + "'. Expected 'y', 'M', 'd', 'h', or 'm'." ); } } int DateTime::year() const { return _year; } int DateTime::month() const { return _month; } int DateTime::day() const { return _day; } int DateTime::hour() const { return _hour; } int DateTime::minute() const { return _minute; } int DateTime::second() const { return _second; } void DateTime::setYear(int y) { _year = y; } void DateTime::setMonth(int m) { _month = m; } void DateTime::setDay(int d) { _day = d; } void DateTime::setHour(int h) { _hour = h; } void DateTime::setMinute(int m) { _minute = m; } void DateTime::setSecond(int s) { _second = (s > 0) ? ((s <= 59) ? s : 59) : 0; } TimeQuantizer::TimeQuantizer(std::string start, std::string end, const std::string& resolution) : _start(start) , _timerange(std::move(start), std::move(end)) { verifyStartTimeRestrictions(); _resolution = parseTimeResolutionStr(resolution); } double TimeQuantizer::parseTimeResolutionStr(const std::string& resolutionStr) { const char unit = resolutionStr.back(); std::string numberString = resolutionStr.substr(0, resolutionStr.length() - 1); char* p; double value = strtol(numberString.c_str(), &p, 10); _resolutionValue = value; _resolutionUnit = unit; if (*p) { // not a number throw ghoul::RuntimeError("Cannot convert " + numberString + " to number"); } else { verifyResolutionRestrictions(static_cast(value), unit); return computeSecondsFromResolution(static_cast(value), unit); } } void TimeQuantizer::setStartEndRange(const std::string& start, const std::string& end) { _start.setTime(start); verifyStartTimeRestrictions(); _timerange.setStart(start); _timerange.setEnd(end); } void TimeQuantizer::setResolution(const std::string& resolutionString) { _resolution = parseTimeResolutionStr(resolutionString); } void TimeQuantizer::verifyStartTimeRestrictions() { if (_start.day() < 1 || _start.day() > 28) { throw ghoul::RuntimeError(fmt::format( "Invalid start day value of {} for day of month. Valid days are 1 - 28", _start.day() )); } if (_start.hour() != 0 || _start.minute() != 0 || _start.second() != 0) { throw ghoul::RuntimeError(fmt::format( "Invalid start time value of {}:{}:{}. Time must be 00:00:00", _start.hour(), _start.minute(), _start.second() )); } } void TimeQuantizer::verifyResolutionRestrictions(const int value, const char unit) { switch (unit) { case 'y': break; case 'M': if (value < 1 || value > 6) { throw ghoul::RuntimeError(fmt::format( "Invalid resolution count of {} for (M)onth option. Valid counts are " "1, 2, 3, 4, or 6", value )); } break; case 'd': if (value < 1 || value > 28) { throw ghoul::RuntimeError(fmt::format( "Invalid resolution count of {} for (d)ay option. Valid counts are " "1 - 28", value )); } break; case 'h': if (!((value >= 1 && value <= 4) || value == 6 || value == 12)) { throw ghoul::RuntimeError(fmt::format( "Invalid resolution count of {} for (h)our option. Valid counts are " "1, 2, 3, 4, 6, or 12", value )); } break; case 'm': if (value != 15 && value != 30) { throw ghoul::RuntimeError(fmt::format( "Invalid resolution count of {} for (m)inute option. Valid counts " "are 15 or 30", value )); } break; default: throw ghoul::RuntimeError(fmt::format( "Invalid unit format '{}'. Expected 'y', 'M', 'd', 'h', or 'm'", unit )); } } double TimeQuantizer::computeSecondsFromResolution(const int valueIn, const char unit) { double value = static_cast(valueIn); // convert value to seconds, based on unit. // The switch statment has intentional fall throughs switch (unit) { case 'y': value *= 365; [[fallthrough]]; case 'd': value *= 24.0; [[fallthrough]]; case 'h': value *= 60.0; [[fallthrough]]; case 'm': value *= 60.0; [[fallthrough]]; case 's': value *= 1.0; break; case 'M': value *= (30.4 * 24.0 * 60.0 * 60.0); break; default: throw ghoul::RuntimeError(fmt::format( "Invalid resolution unit format '{}'. Expected 'y', 'M', 'd', 'h', 'm', " "or 's'", unit )); } return value; } bool TimeQuantizer::quantize(Time& t, bool clamp) { ZoneScoped constexpr const char Format[] = "YYYY-MM-DDTHR:MN:SC.###"; constexpr const int BufferSize = sizeof(Format); char unquantizedString[BufferSize]; std::memset(unquantizedString, 0, BufferSize); SpiceManager::ref().dateFromEphemerisTime( t.j2000Seconds(), unquantizedString, BufferSize, Format ); DateTime unquantized(std::string_view(unquantizedString, BufferSize)); // resolutionFraction helps to improve iteration performance constexpr const double ResolutionFraction = 0.7; constexpr const int IterationLimit = 50; if (_timerange.includes(t)) { int iterations = 0; int lastIncr = 0; int lastDecr = 0; DateTime quantized = DateTime(_timerange.start()); doFirstApproximation(quantized, unquantized, _resolutionValue, _resolutionUnit); double error = unquantized.J2000() - quantized.J2000(); while (error > (_resolution * ResolutionFraction) || error < 0) { if (error > 0) { lastIncr = quantized.increment( static_cast(_resolutionValue), _resolutionUnit, error, _resolution ); } else if (error < 0) { lastDecr = quantized.decrement( static_cast(_resolutionValue), _resolutionUnit, error, _resolution ); } error = unquantized.J2000() - quantized.J2000(); bool hasSettled = (lastIncr == 1 && lastDecr == 1 && error >= 0.0); iterations++; if (hasSettled || iterations > IterationLimit) { break; } } char Buffer[20]; std::memset(Buffer, 0, 20); fmt::format_to( Buffer, "{:0>4}-{:0>2}-{:0>2}T{:0>2}:{:0>2}:{:0>2}", quantized.year(), quantized.month(), quantized.day(), quantized.hour(), quantized.minute(), quantized.second() ); t.setTime(_timerange.clamp(Buffer)); return true; } else if (clamp) { t.setTime(_timerange.clamp(unquantizedString)); return true; } else { return false; } } void TimeQuantizer::doFirstApproximation(DateTime& quantized, DateTime& unQ, double value, char unit) { ZoneScoped switch (unit) { case 'y': { const double minYearsToAdjust = static_cast( unQ.year()) - static_cast(_start.year() ); const double minIncrementsAdjust = minYearsToAdjust / value; quantized.setYear( _start.year() + static_cast(minIncrementsAdjust * value) ); break; } case 'M': { bool isSimMonthPastQuantizedMonth = unQ.month() > static_cast(value); quantized.setYear( (isSimMonthPastQuantizedMonth) ? unQ.year() : unQ.year() - 1 ); break; } case 'd': { const double error = (unQ.J2000() - quantized.J2000()) / (60.0 * 60.0 * 24.0); const int originalHour = quantized.hour(); const int originalMinute = quantized.minute(); const int originalSecond = quantized.second(); const double addToTime = std::round(error) * 86400; Time testDay(quantized.J2000() + addToTime); char Buffer[25]; testDay.ISO8601(Buffer); quantized.setTime(std::string_view(Buffer, 25)); quantized.setHour(originalHour); quantized.setMinute(originalMinute); quantized.setSecond(originalSecond); break; } case 'h': { quantized = unQ; quantized.setMinute(0); quantized.setSecond(0); if (unQ.hour() >= 12) { quantized.setHour(0); } else { quantized.decrementOnce(1, 'd'); } break; } case 'm': quantized = unQ; quantized.setMinute(0); quantized.setSecond(0); if (quantized.hour() > 0) { quantized.decrementOnce(1, 'h'); } else { quantized.decrementOnce(1, 'd'); } break; default: throw ghoul::RuntimeError(fmt::format( "Invalid unit '{}'. Expected 'y', 'M', 'd', 'h', or 'm'", unit )); } } std::vector TimeQuantizer::quantized(Time& start, Time& end) { DateTime s(start.ISO8601()); quantize(start, true); DateTime e(end.ISO8601()); quantize(end, true); const double startSeconds = s.J2000(); const double endSeconds = e.J2000(); const double delta = endSeconds - startSeconds; ghoul_assert( static_cast(delta) % static_cast(_resolution) == 0, "Quantization error" ); std::vector result; DateTime itr = s; RangedTime range(std::string(start.ISO8601()), std::string(end.ISO8601())); while (range.includes(Time(itr.ISO8601()))) { itr.incrementOnce(static_cast(_resolutionValue), _resolutionUnit); result.push_back(itr.ISO8601()); } return result; } } // namespace openspace::globebrowsing