Files
OpenSpace/modules/globebrowsing/src/timequantizer.cpp
Alexander Bock 7004c02b86 Happy new year
2021-01-02 15:26:51 +01:00

634 lines
20 KiB
C++

/*****************************************************************************************
* *
* 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 <modules/globebrowsing/src/timequantizer.h>
#include <openspace/util/spicemanager.h>
#include <openspace/util/time.h>
#include <ghoul/fmt.h>
#include <ghoul/glm.h>
#include <ghoul/misc/assert.h>
#include <ghoul/misc/exception.h>
#include <ghoul/misc/profiling.h>
#include <date/date.h>
#include <algorithm>
#include <charconv>
#include <iomanip>
#include <sstream>
// @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<int>(static_cast<unsigned>(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<int>(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<int>(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<int>(value), unit);
return computeSecondsFromResolution(static_cast<int>(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<double>(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<int>(_resolutionValue),
_resolutionUnit,
error,
_resolution
);
}
else if (error < 0) {
lastDecr = quantized.decrement(
static_cast<int>(_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<double>(
unQ.year()) - static_cast<double>(_start.year()
);
const double minIncrementsAdjust = minYearsToAdjust / value;
quantized.setYear(
_start.year() + static_cast<int>(minIncrementsAdjust * value)
);
break;
}
case 'M':
{
bool isSimMonthPastQuantizedMonth = unQ.month() > static_cast<int>(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<std::string> 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<int>(delta) % static_cast<int>(_resolution) == 0,
"Quantization error"
);
std::vector<std::string> result;
DateTime itr = s;
RangedTime range(std::string(start.ISO8601()), std::string(end.ISO8601()));
while (range.includes(Time(itr.ISO8601()))) {
itr.incrementOnce(static_cast<int>(_resolutionValue), _resolutionUnit);
result.push_back(itr.ISO8601());
}
return result;
}
} // namespace openspace::globebrowsing