174 lines
6.7 KiB
C++
174 lines
6.7 KiB
C++
#include "util/timezones.hpp"
|
|
#include "util/exception.hpp"
|
|
#include "util/geojson_validation.hpp"
|
|
#include "util/log.hpp"
|
|
|
|
#include <boost/filesystem/fstream.hpp>
|
|
#include <boost/filesystem/path.hpp>
|
|
|
|
#include <rapidjson/document.h>
|
|
#include <rapidjson/error/en.h>
|
|
#include <rapidjson/istreamwrapper.h>
|
|
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <regex>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
|
|
#include <time.h>
|
|
|
|
// Function loads time zone shape polygons, computes a zone local time for utc_time,
|
|
// creates a lookup R-tree and returns a lambda function that maps a point
|
|
// to the corresponding local time
|
|
namespace osrm::updater
|
|
{
|
|
|
|
Timezoner::Timezoner(const char geojson[], std::time_t utc_time_now)
|
|
{
|
|
util::Log() << "Time zone validation based on UTC time : " << utc_time_now;
|
|
rapidjson::Document doc;
|
|
rapidjson::ParseResult ok = doc.Parse(geojson);
|
|
if (!ok)
|
|
{
|
|
auto code = ok.Code();
|
|
auto offset = ok.Offset();
|
|
throw osrm::util::exception("Failed to parse timezone geojson with error code " +
|
|
std::to_string(code) + " malformed at offset " +
|
|
std::to_string(offset));
|
|
}
|
|
LoadLocalTimesRTree(doc, utc_time_now);
|
|
}
|
|
|
|
Timezoner::Timezoner(const boost::filesystem::path &tz_shapes_filename, std::time_t utc_time_now)
|
|
{
|
|
util::Log() << "Time zone validation based on UTC time : " << utc_time_now;
|
|
|
|
if (tz_shapes_filename.empty())
|
|
throw osrm::util::exception("Missing time zone geojson file");
|
|
std::ifstream file(tz_shapes_filename.string());
|
|
if (!file.is_open())
|
|
throw osrm::util::exception("failed to open " + tz_shapes_filename.string());
|
|
|
|
util::Log() << "Parsing " + tz_shapes_filename.string();
|
|
rapidjson::IStreamWrapper isw(file);
|
|
rapidjson::Document geojson;
|
|
geojson.ParseStream(isw);
|
|
if (geojson.HasParseError())
|
|
{
|
|
throw osrm::util::exception(std::string("Failed to parse ") + tz_shapes_filename.string() +
|
|
":" + std::to_string(geojson.GetErrorOffset()) + " error: " +
|
|
rapidjson::GetParseError_En(geojson.GetParseError()));
|
|
}
|
|
LoadLocalTimesRTree(geojson, utc_time_now);
|
|
}
|
|
|
|
void Timezoner::LoadLocalTimesRTree(rapidjson::Document &geojson, std::time_t utc_time)
|
|
{
|
|
if (!geojson.HasMember("type"))
|
|
throw osrm::util::exception("Failed to parse time zone file. Missing type member.");
|
|
if (!geojson["type"].IsString())
|
|
throw osrm::util::exception(
|
|
"Failed to parse time zone file. Missing string-based type member.");
|
|
if (std::strcmp(geojson["type"].GetString(), "FeatureCollection") != 0)
|
|
throw osrm::util::exception(
|
|
"Failed to parse time zone file. Geojson is not of FeatureCollection type");
|
|
if (!geojson.HasMember("features"))
|
|
throw osrm::util::exception("Failed to parse time zone file. Missing features list.");
|
|
|
|
// Lambda function that returns local time in the tzname time zone
|
|
// Thread safety: MT-Unsafe const:env
|
|
std::unordered_map<std::string, struct tm> local_time_memo;
|
|
auto get_local_time_in_tz = [utc_time, &local_time_memo](const char *tzname)
|
|
{
|
|
auto it = local_time_memo.find(tzname);
|
|
if (it == local_time_memo.end())
|
|
{
|
|
struct tm timeinfo;
|
|
#if defined(_WIN32)
|
|
_putenv_s("TZ", tzname);
|
|
_tzset();
|
|
localtime_s(&timeinfo, &utc_time);
|
|
#else
|
|
setenv("TZ", tzname, 1);
|
|
tzset();
|
|
localtime_r(&utc_time, &timeinfo);
|
|
#endif
|
|
it = local_time_memo.insert({tzname, timeinfo}).first;
|
|
}
|
|
|
|
return it->second;
|
|
};
|
|
BOOST_ASSERT(geojson["features"].IsArray());
|
|
const auto &features_array = geojson["features"].GetArray();
|
|
std::vector<rtree_t::value_type> polygons;
|
|
for (rapidjson::SizeType i = 0; i < features_array.Size(); i++)
|
|
{
|
|
util::validateFeature(features_array[i]);
|
|
|
|
// time zone geojson specific checks
|
|
const auto &feature = features_array[i].GetObject();
|
|
const auto &properties = feature["properties"].GetObject();
|
|
std::string tzid_key = "tzid";
|
|
if (properties.HasMember("tzid"))
|
|
{
|
|
if (!properties["tzid"].IsString())
|
|
throw osrm::util::exception("Feature has non-string 'tzid' value.");
|
|
tzid_key = "tzid";
|
|
}
|
|
else if (properties.HasMember("TZID"))
|
|
{
|
|
if (!properties["TZID"].IsString())
|
|
throw osrm::util::exception("Feature has non-string 'TZID' value.");
|
|
tzid_key = "TZID";
|
|
}
|
|
else
|
|
{
|
|
throw osrm::util::exception("Feature is missing 'tzid' member in properties.");
|
|
}
|
|
|
|
// Case-sensitive check of type https://tools.ietf.org/html/rfc7946#section-1.4
|
|
const auto &geometry = feature["geometry"].GetObject();
|
|
if (std::strcmp(geometry["type"].GetString(), "Polygon") == 0)
|
|
{
|
|
// The first array of polygon coords is the exterior ring, we only want to access that
|
|
// https://tools.ietf.org/html/rfc7946#section-3.1.6
|
|
polygon_t polygon;
|
|
const auto &coords_outer_array = geometry["coordinates"].GetArray()[0].GetArray();
|
|
for (rapidjson::SizeType i = 0; i < coords_outer_array.Size(); ++i)
|
|
{
|
|
util::validateCoordinate(coords_outer_array[i]);
|
|
const auto &coords = coords_outer_array[i].GetArray();
|
|
polygon.outer().emplace_back(coords[0].GetDouble(), coords[1].GetDouble());
|
|
}
|
|
polygons.emplace_back(boost::geometry::return_envelope<box_t>(polygon),
|
|
local_times.size());
|
|
|
|
// Get time zone name and emplace polygon and local time for the UTC input
|
|
const auto &tzname = properties[tzid_key.c_str()].GetString();
|
|
local_times.push_back(local_time_t{polygon, get_local_time_in_tz(tzname)});
|
|
}
|
|
else
|
|
{
|
|
util::Log(logDEBUG) << "Skipping non-polygon shape in timezone file";
|
|
}
|
|
}
|
|
util::Log() << "Parsed " << polygons.size() << "time zone polygons." << std::endl;
|
|
// Create R-tree for collected shape polygons
|
|
rtree = rtree_t(polygons);
|
|
}
|
|
|
|
std::optional<struct tm> Timezoner::operator()(const point_t &point) const
|
|
{
|
|
std::vector<rtree_t::value_type> result;
|
|
rtree.query(boost::geometry::index::intersects(point), std::back_inserter(result));
|
|
for (const auto &v : result)
|
|
{
|
|
const auto index = v.second;
|
|
if (boost::geometry::within(point, local_times[index].first))
|
|
return local_times[index].second;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
} // namespace osrm::updater
|