From 085f82a105697f93dcb5d06d754de5354e1a0ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Torrente?= Date: Sun, 1 Sep 2024 11:53:48 +0200 Subject: [PATCH] Timeutils and RageSet calculation --- CMakeLists.txt | 1 + adagucserverEC/CMakeLists.txt | 7 + adagucserverEC/CXMLGen.cpp | 251 ++++++++---------- adagucserverEC/testtimeutils.cpp | 126 +++++++++ adagucserverEC/timeutils.cpp | 200 ++++++++++++++ adagucserverEC/timeutils.h | 37 +++ .../test_WCSDescribeCoverage_testdatanc.xml | 33 +-- 7 files changed, 498 insertions(+), 157 deletions(-) create mode 100644 adagucserverEC/testtimeutils.cpp create mode 100644 adagucserverEC/timeutils.cpp create mode 100644 adagucserverEC/timeutils.h diff --git a/CMakeLists.txt b/CMakeLists.txt index c6cd2847..2d4472ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ add_executable(geojsondump ./adagucserverEC/geojsondump.cpp) add_test(testhclasses hclasses/testhclasses) add_test(testadagucserver adagucserverEC/testadagucserver) +add_test(testtimeutils adagucserverEC/testtimeutils) target_link_libraries(adagucserver adagucserverEC hclasses CCDFDataModel) target_link_libraries(h5ncdump adagucserverEC hclasses CCDFDataModel) diff --git a/adagucserverEC/CMakeLists.txt b/adagucserverEC/CMakeLists.txt index 942d0bc4..c7643f60 100644 --- a/adagucserverEC/CMakeLists.txt +++ b/adagucserverEC/CMakeLists.txt @@ -182,6 +182,9 @@ add_library( Types/ProjectionStore.h Types/ProjectionStore.cpp testadagucserver.cpp + testtimeutils.cpp + timeutils.cpp + timeutils.h ) target_include_directories(adagucserverEC PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${Cairo_INCLUDE_DIRS} ${FREETYPE_INCLUDE_DIRS} ${PostgreSQL_INCLUDE_DIRS} ${GDAL_INCLUDE_DIRS} ${PROJ_INCLUDE_DIR}) @@ -190,3 +193,7 @@ target_link_libraries(adagucserverEC CCDFDataModel hclasses ${NetCDF_LIBRARIES} # Build unit test executable add_executable(testadagucserver testadagucserver.cpp) target_link_libraries(testadagucserver PRIVATE adagucserverEC CCDFDataModel hclasses CppUnitLite) + +# Build unit tests for timeutils +add_executable(testtimeutils testtimeutils.cpp) +target_link_libraries(testtimeutils PRIVATE adagucserverEC hclasses CppUnitLite) \ No newline at end of file diff --git a/adagucserverEC/CXMLGen.cpp b/adagucserverEC/CXMLGen.cpp index 062ce81a..a365b66f 100644 --- a/adagucserverEC/CXMLGen.cpp +++ b/adagucserverEC/CXMLGen.cpp @@ -25,10 +25,12 @@ #include #include +#include #include #include "CXMLGen.h" #include "CDBFactory.h" #include "LayerTypeLiveUpdate/LayerTypeLiveUpdate.h" +#include "timeutils.h" // #define CXMLGEN_DEBUG // #define MEASURE_TIME const char *CFile::className = "CFile"; @@ -36,6 +38,35 @@ const char *CFile::className = "CFile"; const char *CXMLGen::className = "CXMLGen"; int CXMLGen::WCSDescribeCoverage(CServerParams *srvParam, CT::string *XMLDocument) { return OGCGetCapabilities(srvParam, XMLDocument); } +// Function to parse a string to double if numeric +double parseNumeric(std::string const &str, bool &isNumeric) { + auto result = double(); + auto i = std::istringstream(str); + i >> result; + isNumeric = !i.fail() && i.eof(); + return result; +} + +// Sort values that can either be numeric of a string +bool multiTypeSort(const CT::string &a, const CT::string &b) { + // Try to convert strings to numbers + float aNum, bNum; + bool isANum, isBNum; + + isANum = false; + aNum = parseNumeric(a.c_str(), isANum); + + isBNum = false; + bNum = parseNumeric(b.c_str(), isBNum); + + // Do numerical comparison or alphabetical comparison according to type + if (isANum && isBNum) { + return aNum < bNum; + } else { + return a < b; + } +} + bool compareStringCase(const std::string &s1, const std::string &s2) { return strcmp(s1.c_str(), s2.c_str()) <= 0; } const WMSLayer *getFirstLayerWithoutError(std::vector *myWMSLayerList) { @@ -446,133 +477,21 @@ int CXMLGen::getDimsForLayer(WMSLayer *myWMSLayer) { if (store != NULL) { if (store->size() != 0) { dataHasBeenFoundInStore = true; - tm tms[store->size()]; - try { - + // Retrieve all timestamps + std::vector isoTimes; for (size_t j = 0; j < store->size(); j++) { store->getRecord(j)->get("time")->setChar(10, 'T'); const char *isotime = store->getRecord(j)->get("time")->c_str(); -#ifdef CXMLGEN_DEBUG - // CDBDebug("isotime = %s",isotime); -#endif - CT::string year, month, day, hour, minute, second; - year.copy(isotime + 0, 4); - tms[j].tm_year = year.toInt() - 1900; - month.copy(isotime + 5, 2); - tms[j].tm_mon = month.toInt() - 1; - day.copy(isotime + 8, 2); - tms[j].tm_mday = day.toInt(); - hour.copy(isotime + 11, 2); - tms[j].tm_hour = hour.toInt(); - minute.copy(isotime + 14, 2); - tms[j].tm_min = minute.toInt(); - second.copy(isotime + 17, 2); - tms[j].tm_sec = second.toInt(); - } - size_t nrTimes = store->size() - 1; - bool isConst = true; - if (store->size() < 4) { - isConst = false; - } - try { - CTime *time = CTime::GetCTimeInstance(myWMSLayer->dataSource->getDataObject(0)->cdfObject->getVariable("time")); - if (time == nullptr) { - CDBDebug(CTIME_GETINSTANCE_ERROR_MESSAGE); - return 1; - } - if (time->getMode() != 0) { - isConst = false; - } - } catch (int e) { - } - - CT::string iso8601timeRes = "P"; - CT::string yearPart = ""; - if (tms[1].tm_year - tms[0].tm_year != 0) { - if (tms[1].tm_year - tms[0].tm_year == (tms[nrTimes < 10 ? nrTimes : 10].tm_year - tms[0].tm_year) / double(nrTimes < 10 ? nrTimes : 10)) { - yearPart.printconcat("%dY", abs(tms[1].tm_year - tms[0].tm_year)); - } else { - isConst = false; -#ifdef CXMLGEN_DEBUG - CDBDebug("year is irregular"); -#endif - } - } - if (tms[1].tm_mon - tms[0].tm_mon != 0) { - if (tms[1].tm_mon - tms[0].tm_mon == (tms[nrTimes < 10 ? nrTimes : 10].tm_mon - tms[0].tm_mon) / double(nrTimes < 10 ? nrTimes : 10)) - yearPart.printconcat("%dM", abs(tms[1].tm_mon - tms[0].tm_mon)); - else { - isConst = false; -#ifdef CXMLGEN_DEBUG - CDBDebug("month is irregular"); -#endif - } - } - - if (tms[1].tm_mday - tms[0].tm_mday != 0) { - if (tms[1].tm_mday - tms[0].tm_mday == (tms[nrTimes < 10 ? nrTimes : 10].tm_mday - tms[0].tm_mday) / double(nrTimes < 10 ? nrTimes : 10)) - yearPart.printconcat("%dD", abs(tms[1].tm_mday - tms[0].tm_mday)); - else { - isConst = false; -#ifdef CXMLGEN_DEBUG - CDBDebug("day irregular"); - for (size_t j = 0; j < nrTimes; j++) { - CDBDebug("Day %d = %d", j, tms[j].tm_mday); - } -#endif - } - } - - CT::string hourPart = ""; - if (tms[1].tm_hour - tms[0].tm_hour != 0) { - hourPart.printconcat("%dH", abs(tms[1].tm_hour - tms[0].tm_hour)); - } - if (tms[1].tm_min - tms[0].tm_min != 0) { - hourPart.printconcat("%dM", abs(tms[1].tm_min - tms[0].tm_min)); - } - if (tms[1].tm_sec - tms[0].tm_sec != 0) { - hourPart.printconcat("%dS", abs(tms[1].tm_sec - tms[0].tm_sec)); - } - - int sd = (tms[1].tm_hour * 3600 + tms[1].tm_min * 60 + tms[1].tm_sec) - (tms[0].tm_hour * 3600 + tms[0].tm_min * 60 + tms[0].tm_sec); - for (size_t j = 2; j < store->size() && isConst; j++) { - int d = (tms[j].tm_hour * 3600 + tms[j].tm_min * 60 + tms[j].tm_sec) - (tms[j - 1].tm_hour * 3600 + tms[j - 1].tm_min * 60 + tms[j - 1].tm_sec); - if (d > 0) { - if (sd != d) { - isConst = false; -#ifdef CXMLGEN_DEBUG - CDBDebug("hour/min/sec is irregular %d ", j); -#endif - } - } + isoTimes.push_back(isotime); } - - // Check whether we found a time resolution - if (isConst == false) { - hasMultipleValues = true; -#ifdef CXMLGEN_DEBUG - CDBDebug("Not a continous time dimension, multipleValues required"); -#endif - } else { -#ifdef CXMLGEN_DEBUG - CDBDebug("Continous time dimension, Time resolution needs to be calculated"); -#endif - hasMultipleValues = false; - } - - if (isConst) { - if (yearPart.length() > 0) { - iso8601timeRes.concat(&yearPart); - } - if (hourPart.length() > 0) { - iso8601timeRes.concat("T"); - iso8601timeRes.concat(&hourPart); - } + CT::string estimatedISODuration = estimateISO8601Duration(isoTimes); + hasMultipleValues = estimatedISODuration.empty(); #ifdef CXMLGEN_DEBUG - CDBDebug("Calculated a timeresolution of %s", iso8601timeRes.c_str()); + CDBDebug("Estimated an ISO duration of %s with length %d", estimatedISODuration.c_str(), estimatedISODuration.length()); #endif - myWMSLayer->dataSource->cfgLayer->Dimension[i]->attr.interval.copy(iso8601timeRes.c_str()); + if (estimatedISODuration.length() > 0) { // Check if estimatedISODuration is not an empty string + myWMSLayer->dataSource->cfgLayer->Dimension[i]->attr.interval.copy(estimatedISODuration.c_str()); myWMSLayer->dataSource->cfgLayer->Dimension[i]->attr.units.copy("ISO8601"); } } catch (int e) { @@ -1561,6 +1480,79 @@ int CXMLGen::getWCS_1_0_0_Capabilities(CT::string *XMLDoc, std::vectordimList.size()) { + return; + } + XMLDoc->concat(" \n" + " \n" + " dimensions\n" + " \n"); + // Dims + for (size_t d = 0; d < layer->dimList.size(); d++) { + WMSLayer::Dim *dim = layer->dimList[d]; + CT::string min, max, duration; + CT::string *valueSplit; + std::vector valuesVector; + + // Case of min/max(/duration), for time dimension + valueSplit = dim->values.splitToArray("/"); + if (valueSplit->count >= 2) { + min = valueSplit[0]; + max = valueSplit[1]; + // Third value is the interval duration + if (valueSplit->count == 3) { + duration = valueSplit[2]; + } + } else { + // General case of a list of values (of any type) + valueSplit = dim->values.splitToArray(","); + valuesVector = std::vector(valueSplit, valueSplit + valueSplit->count); + std::sort(valuesVector.begin(), valuesVector.end(), multiTypeSort); + min = valuesVector[0]; + max = valuesVector.back(); + } + + XMLDoc->printconcat(" \n" + " \n" + " %s\n" + " \n", + dim->name.c_str(), dim->name.c_str()); + if (valueSplit->count >= 2) { + XMLDoc->printconcat(" \n" + " \n" + " %s\n" + " %s\n", + min.c_str(), max.c_str()); + // Precalculate the interval in the case of time (no interval if fewer than 4 values) + if ((dim->name.indexOf("time") != -1) && duration.length() > 0) { + XMLDoc->printconcat(" %s\n", duration.c_str()); // .c_str()); + } + XMLDoc->printconcat(" \n"); + // Print all possible values if there is a relatively small number, for other dimensions + if ((valueSplit->count <= 100) && (dim->name.indexOf("time") == -1)) { + for (size_t i = 0; i < valueSplit->count; i++) { + XMLDoc->printconcat(" %s\n", valuesVector[i].c_str()); + } + } + XMLDoc->printconcat(" \n"); + } + XMLDoc->printconcat(" \n" + " \n"); + delete[] valueSplit; + } + + XMLDoc->concat(" \n" + " \n"); +} + int CXMLGen::getWCS_1_0_0_DescribeCoverage(CT::string *XMLDoc, std::vector *myWMSLayerList) { XMLDoc->copy("\n" @@ -1687,22 +1679,9 @@ int CXMLGen::getWCS_1_0_0_DescribeCoverage(CT::string *XMLDoc, std::vectorconcat(" \n"); } - XMLDoc->concat(" \n" - " \n" - " \n" - " bands\n" - " \n" - " \n" - " \n" - " bands\n" - " \n" - " \n" - " 1\n" - " \n" - " \n" - " \n" - " \n" - " \n"); + XMLDoc->concat(" \n"); + // Generate the XML code for RangeSet, including dimensions (AxisDescriptions) + generateRangeSet(XMLDoc, layer); // Supported CRSs XMLDoc->concat(" \n"); diff --git a/adagucserverEC/testtimeutils.cpp b/adagucserverEC/testtimeutils.cpp new file mode 100644 index 00000000..e89919a7 --- /dev/null +++ b/adagucserverEC/testtimeutils.cpp @@ -0,0 +1,126 @@ +/****************************************************************************** + * + * Copyright 2024, Royal Netherlands Meteorological Institute (KNMI) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +#include "CDebugger.h" +#include "CppUnitLite/TestHarness.h" +#include "CTString.h" +#include "timeutils.h" + +TEST(ParseISOTimestamp, TimeUtils) { + CT::string isotime("2024-07-29T15:30:45"); + std::tm expected = {}; + expected.tm_year = 2024 - 1900; + expected.tm_mon = 7 - 1; + expected.tm_mday = 29; + expected.tm_hour = 15; + expected.tm_min = 30; + expected.tm_sec = 45; + + std::tm result = parseISOTimestamp(isotime); + + CHECK(result.tm_year == expected.tm_year); + CHECK(result.tm_mon == expected.tm_mon); + CHECK(result.tm_mday == expected.tm_mday); + CHECK(result.tm_hour == expected.tm_hour); + CHECK(result.tm_min == expected.tm_min); + CHECK(result.tm_sec == expected.tm_sec); +} + +TEST(CalculateTimeInterval, TimeUtils) { + std::tm start = {}; + start.tm_min = 30; + start.tm_hour = 15; + start.tm_mday = 29; + start.tm_mon = 7; + start.tm_year = 2024 - 1900; + std::tm end = {}; + end.tm_sec = 45; + end.tm_min = 45; + end.tm_hour = 16; + end.tm_mday = 29; + end.tm_mon = 7; + end.tm_year = 2025 - 1900; + + TimeInterval expected = {1, 0, 0, 1, 15, 45}; + TimeInterval result = calculateTimeInterval(start, end); + + CHECK(result.years == expected.years); + CHECK(result.months == expected.months); + CHECK(result.days == expected.days); + CHECK(result.hours == expected.hours); + CHECK(result.minutes == expected.minutes); + CHECK(result.seconds == expected.seconds); +} + +TEST(ToISO8601Interval, TimeUtils) { + TimeInterval interval = {1, 2, 10, 5, 30, 45}; + CT::string expected("P1Y2M10DT5H30M45S"); + + CT::string result = toISO8601Interval(interval); + CHECK(result == expected); +} + +TEST(ToSeconds, TimeUtils) { + TimeInterval interval = {0, 1, 0, 1, 0, 20}; // 1 month, 1 hour, 20 seconds + long expected = 2595620; + + long result = toSeconds(interval); + CHECK(result == expected); +} + +TEST(EstimateISO8601Duration, TimeUtils) { + // Most straightforward case + std::vector timestamps = {"2024-07-29T15:30:45", "2024-07-29T16:30:45", "2024-07-29T17:30:45", "2024-07-29T18:30:45"}; + CT::string expected("PT1H"); + CT::string result = estimateISO8601Duration(timestamps); + CHECK(result == expected); + + // Most straightforward case, with UTC z + std::vector timestamps_utc = {"2024-07-29T15:30:45Z", "2024-07-29T16:30:45Z", "2024-07-29T17:30:45Z", "2024-07-29T18:30:45Z"}; + CT::string expected_utc("PT1H"); + CT::string result_utc = estimateISO8601Duration(timestamps_utc); + CHECK(result_utc == expected_utc); + + // Case with one timestep missing + std::vector timestamps_with_gap = {"2024-07-29T15:30:45", "2024-07-29T16:30:45", "2024-07-29T18:30:45", "2024-07-29T19:30:45", + "2024-07-29T20:30:45", "2024-07-29T21:30:45", "2024-07-29T22:30:45", "2024-07-29T23:30:45"}; + CT::string expected_with_gap("PT1H"); + CT::string result_with_gap = estimateISO8601Duration(timestamps_with_gap); + CHECK(result_with_gap == expected_with_gap); + + // Case in February of a leap year + std::vector leap_year_timestamps = {"2024-02-28T23:30:45", "2024-02-29T00:30:45", "2024-02-29T01:30:45", "2024-02-29T02:30:45"}; + CT::string expected_leap_year("PT1H"); + CT::string result_leap_year = estimateISO8601Duration(leap_year_timestamps); + CHECK(result_leap_year == expected_leap_year); + + // Case of a complex interval (5 days and 10 hours) + std::vector complex_interval_timestamps = {"2024-07-01T08:00:00", "2024-07-06T18:00:00", "2024-07-12T04:00:00", "2024-07-17T14:00:00", "2024-07-23T00:00:00"}; + CT::string expected_complex_interval("P5DT10H"); + CT::string result_complex_interval = estimateISO8601Duration(complex_interval_timestamps); + CHECK(result_complex_interval == expected_complex_interval); +} + +int main() { + TestResult tr; + TestRegistry::runAllTests(tr); + if (tr.failureCount != 0) { + return 1; + } + return 0; +} diff --git a/adagucserverEC/timeutils.cpp b/adagucserverEC/timeutils.cpp new file mode 100644 index 00000000..287173df --- /dev/null +++ b/adagucserverEC/timeutils.cpp @@ -0,0 +1,200 @@ +/****************************************************************************** + * + * Copyright 2024, Royal Netherlands Meteorological Institute (KNMI) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +#include +#include +#include "timeutils.h" + +std::tm parseISOTimestamp(const CT::string &isotime) { + std::tm timeStruct = {}; + // Tokenize into individual fields + CT::string year, month, day, hour, minute, second; + year.copy(isotime + 0, 4); + month.copy(isotime + 5, 2); + day.copy(isotime + 8, 2); + hour.copy(isotime + 11, 2); + minute.copy(isotime + 14, 2); + second.copy(isotime + 17, 2); + + // Build time struct + timeStruct.tm_year = year.toInt() - 1900; + timeStruct.tm_mon = month.toInt() - 1; + timeStruct.tm_mday = day.toInt(); + timeStruct.tm_hour = hour.toInt(); + timeStruct.tm_min = minute.toInt(); + timeStruct.tm_sec = second.toInt(); + + return timeStruct; +} + +TimeInterval calculateTimeInterval(const std::tm &start, const std::tm &end) { + TimeInterval interval = {0, 0, 0, 0, 0, 0}; + + interval.seconds = end.tm_sec - start.tm_sec; + interval.minutes = end.tm_min - start.tm_min; + interval.hours = end.tm_hour - start.tm_hour; + interval.days = end.tm_mday - start.tm_mday; + interval.months = end.tm_mon - start.tm_mon; + interval.years = end.tm_year - start.tm_year; + + // Adjust number of minutes if there are negative seconds, + // meaning there were less than 60 seconds between timestamps. + if (interval.seconds < 0) { + interval.seconds += 60; + interval.minutes--; + } + // Adjust number of hours if there are negative minutes, + // meaning there were less than 60 minutes between timestamps. + if (interval.minutes < 0) { + interval.minutes += 60; + interval.hours--; + } + // Adjust number of days if there are negative hours, + // meaning there were less than 24 hours between timestamps. + if (interval.hours < 0) { + interval.hours += 24; + interval.days--; + } + // Adjust number of days and months using month length + // and leap year calculations, also adjust in case of + // negative days. + if (interval.days < 0) { + // Take month length into consideration (month 0 being January and 11 December) + static const int daysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + int previousMonth = (end.tm_mon == 0) ? 11 : end.tm_mon - 1; + int year = (end.tm_mon == 0) ? end.tm_year - 1 : end.tm_year; + + // Adjust for leap years, using the following rules: + // - In general, a year is a leap year if divisible by 4 + // - In general, end-of-century years (divisible by 100) are NOT + // - But if divisible by 400, it IS a leap year + int daysPrevMonth = daysInMonth[previousMonth]; + if (previousMonth == 1 && ((year + 1900) % 4 == 0 && ((year + 1900) % 100 != 0 || (year + 1900) % 400 == 0))) { + daysPrevMonth = 29; + } + interval.days += daysPrevMonth; + interval.months--; + } + if (interval.months < 0) { + interval.months += 12; + interval.years--; + } + + return interval; +} + +CT::string toISO8601Interval(const TimeInterval &interval) { + // Start with the period 'P' + CT::string result("P"); + + // Append years, months, or days if present + if (interval.years > 0) { + result.printconcat("%dY", interval.years); + } + if (interval.months > 0) { + result.printconcat("%dM", interval.months); + } + if (interval.days > 0) { + result.printconcat("%dD", interval.days); + } + + // Add hour part T if there are hours, minutes, or seconds + if (interval.hours > 0 || interval.minutes > 0 || interval.seconds > 0) { + result += "T"; + + if (interval.hours > 0) { + result.printconcat("%dH", interval.hours); + } + if (interval.minutes > 0) { + result.printconcat("%dM", interval.minutes); + } + if (interval.seconds > 0) { + result.printconcat("%dS", interval.seconds); + } + } + return result; +} + +long long toSeconds(const TimeInterval &interval) { + // Convert a TimeInterval to approximate seconds for sorting + return interval.seconds + interval.minutes * 60 + interval.hours * 3600 + interval.days * 86400 + interval.months * 2592000 + interval.years * 31536000; +} + +CT::string estimateISO8601Duration(const std::vector ×tamps, double threshold) { + // Size considered too small to find a pattern + if (timestamps.size() < 4) return CT::string(""); + + std::vector intervals; + + // Parse all timestamps into tm structs + std::vector parsedTimes; + for (const auto ×tamp : timestamps) { + parsedTimes.push_back(parseISOTimestamp(timestamp)); + } + + // Calculate intervals between consecutive timestamps + for (size_t i = 1; i < parsedTimes.size(); ++i) { + intervals.push_back(calculateTimeInterval(parsedTimes[i - 1], parsedTimes[i])); + } + + // Count occurrences of each interval in terms of total seconds + // Count occurrences of each interval in terms of total seconds + std::map intervalFrequency; + std::map intervalMap; + for (const auto &interval : intervals) { + long long intervalInSeconds = toSeconds(interval); + intervalFrequency[intervalInSeconds]++; + intervalMap[intervalInSeconds] = interval; + } + + // Retrieve the most frequent and smallest interval + // In case of missing incoming values, we will find some occurrences of + // larger intervals. + // Heuristic: This interval has to present itself at least 80% of the time + long long mostFrequentIntervalInSeconds = 0; + int maxFrequency = 0; + // Each entry has [intervalInSeconds,frequency] + for (const auto &entry : intervalFrequency) { + if (entry.second > maxFrequency || (entry.second == maxFrequency && entry.first < mostFrequentIntervalInSeconds)) { + mostFrequentIntervalInSeconds = entry.first; + maxFrequency = entry.second; + } + } + + // Check if all intervals are the same + // const TimeInterval &firstInterval = intervals[0]; + // for (size_t i = 1; i < intervals.size(); ++i) { + // if (intervals[i].years != firstInterval.years || intervals[i].months != firstInterval.months || intervals[i].days != firstInterval.days || intervals[i].hours != firstInterval.hours || + // intervals[i].minutes != firstInterval.minutes || intervals[i].seconds != firstInterval.seconds) { + // return CT::string(""); // No consistent interval found + // } + // } + + // If all intervals are the same, convert the first interval to ISO8601 format + // return toISO8601Interval(firstInterval); + // Check if the most frequent interval meets the threshold + // Check if the most frequent interval meets the threshold + double frequencyPercentage = static_cast(maxFrequency) / intervals.size(); + if (frequencyPercentage < threshold) { + return CT::string(""); // No interval meets the threshold requirement + } + + // Use the most frequent interval + TimeInterval mostFrequentInterval = intervalMap[mostFrequentIntervalInSeconds]; + return toISO8601Interval(mostFrequentInterval); +} diff --git a/adagucserverEC/timeutils.h b/adagucserverEC/timeutils.h new file mode 100644 index 00000000..44c1a7d4 --- /dev/null +++ b/adagucserverEC/timeutils.h @@ -0,0 +1,37 @@ +/****************************************************************************** + * + * Copyright 2024, Royal Netherlands Meteorological Institute (KNMI) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ******************************************************************************/ + +#include "CTString.h" +#include + +// Type to represent the relative interval between two timestamps +struct TimeInterval { + int years; + int months; + int days; + int hours; + int minutes; + int seconds; +}; + +std::tm parseISOTimestamp(const CT::string &isotime); +TimeInterval calculateTimeInterval(const std::tm &start, const std::tm &end); +TimeInterval calculateTimeInterval(const std::tm &start, const std::tm &end); +CT::string toISO8601Interval(const TimeInterval &interval); +long long toSeconds(const TimeInterval &interval); +CT::string estimateISO8601Duration(const std::vector ×tamps, double threshold = 0.8); diff --git a/tests/expectedoutputs/TestWCS/test_WCSDescribeCoverage_testdatanc.xml b/tests/expectedoutputs/TestWCS/test_WCSDescribeCoverage_testdatanc.xml index c91bb3dc..06bc008f 100644 --- a/tests/expectedoutputs/TestWCS/test_WCSDescribeCoverage_testdatanc.xml +++ b/tests/expectedoutputs/TestWCS/test_WCSDescribeCoverage_testdatanc.xml @@ -108,15 +108,12 @@ - bands - + dimensions + - bands - - - 1 - + time + @@ -255,15 +252,12 @@ - bands - + dimensions + - bands - - - 1 - + time + @@ -402,15 +396,12 @@ - bands - + dimensions + - bands - - - 1 - + time +