Program Listing for File cesium.cc¶
↰ Return to documentation for file (interfaces/cesium.cc)
#include "lupnt/interfaces/cesium.h"
// Suppress warnings from crow third-party library
#ifdef __GNUC__
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wuninitialized"
# pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
#ifdef __clang__
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Wuninitialized"
#endif
#include <crow.h>
#ifdef __GNUC__
# pragma GCC diagnostic pop
#endif
#ifdef __clang__
# pragma clang diagnostic pop
#endif
#include <Eigen/Dense>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <sstream>
#include <string>
#include <thread>
#include <vector>
#include "lupnt/conversions/time_conversions.h"
#include "lupnt/core/constants.h"
#include "lupnt/core/definitions.h"
#include "lupnt/core/error.h"
#include "lupnt/core/file.h"
#include "lupnt/core/logger.h"
using json = nlohmann::json;
namespace lupnt {
CesiumViewer::CesiumViewer(int port, const std::string& host) : host_(host), port_(port) {
static_path_ = GetBaseDir() / ".cache" / "cesium";
std::filesystem::create_directories(static_path_);
const char* cesium_token = std::getenv("CESIUM_TOKEN");
if (!cesium_token)
LUPNT_CHECK(false, "CESIUM_TOKEN environment variable is not set", "CesiumViewer");
// Copy index.html template and replace token
std::filesystem::path index_template = GetDataPath() / "CesiumViewer" / "index.html";
std::ifstream in(index_template);
std::string index_content((std::istreambuf_iterator<char>(in)),
std::istreambuf_iterator<char>());
size_t pos = index_content.find("{{ CESIUM_TOKEN }}");
if (pos != std::string::npos) {
index_content.replace(pos, std::string("{{ CESIUM_TOKEN }}").length(), cesium_token);
} else {
LUPNT_CHECK(false, "Failed to replace CESIUM_TOKEN in index.html", "CesiumViewer");
}
std::ofstream out(static_path_ / "index.html");
out << index_content;
out.close();
Logger::Debug("Copied index.html to " + static_path_.string(), "CesiumViewer");
app_.loglevel(crow::LogLevel::Warning);
SetupRoutes();
}
CesiumViewer::~CesiumViewer() { Stop(); }
void CesiumViewer::Stop() {
if (!running_) return;
app_.stop();
Logger::Info("Stopping CesiumViewer", "CesiumViewer");
if (server_thread_.joinable()) server_thread_.join();
running_ = false;
}
void CesiumViewer::Run() {
Logger::Info("Running on http://" + host_ + ":" + std::to_string(port_), "CesiumViewer");
running_ = true;
server_thread_ = std::thread([this]() { app_.port(port_).multithreaded().run(); });
}
void CesiumViewer::AddEntity(const VecX& times, const MatX3& positions,
const std::string& entity_id, const std::string& name,
const std::vector<int>& color, const std::string& description,
BodyId body_id, int size) {
std::string frame = "FIXED";
LUPNT_CHECK(times.size() == positions.rows(),
"Times and positions arrays must have the same length", "CesiumViewer");
std::string initial_time_utc = TimeToGregorianString(UtcToTai(GetLupntEpoch()));
Entity entity{entity_id, name, times, positions, color, description, body_id,
initial_time_utc, size};
std::lock_guard<std::mutex> lock(mutex_);
entities_.push_back(entity);
Logger::Info("Added entity " + entity_id, "CesiumViewer");
}
void CesiumViewer::ClearEntities() {
std::lock_guard<std::mutex> lock(mutex_);
entities_.clear();
Logger::Info("Cleared entities", "CesiumViewer");
}
void CesiumViewer::SetupRoutes() {
Logger::Debug("Setting up routes", "CesiumViewer");
CROW_ROUTE(app_, "/")([this]() {
std::ifstream file(static_path_ / "index.html");
if (!file) return crow::response(404);
std::ostringstream contents;
contents << file.rdbuf();
Logger::Debug("Serving index.html", "CesiumViewer");
return crow::response(contents.str());
});
CROW_ROUTE(app_, "/entities/all")([this]() {
json entities_info = json::array();
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& entity : entities_) {
entities_info.push_back({{"entity_id", entity.id}, {"body_id", entity.body_id}});
}
json result;
result["entities"] = entities_info;
Logger::Debug("Serving entities", "CesiumViewer");
return crow::response(result.dump());
});
CROW_ROUTE(app_, "/entities/timerange")([this]() {
std::lock_guard<std::mutex> lock(mutex_);
if (entities_.empty()) {
return crow::response(R"({"start_time": null, "end_time": null})");
}
std::optional<std::chrono::system_clock::time_point> global_start, global_end;
for (const auto& entity : entities_) {
auto start = ParseIso8601(entity.initial_time_utc);
double last_time = (entity.times.size() == 0)
? 0.0
: static_cast<double>(entity.times(entity.times.size() - 1));
auto stop = start
+ std::chrono::duration_cast<std::chrono::system_clock::duration>(
std::chrono::duration<double>(last_time));
if (!global_start || start < *global_start) global_start = start;
if (!global_end || stop > *global_end) global_end = stop;
}
auto to_iso = [](const std::chrono::system_clock::time_point& tp) {
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::tm tm = *std::gmtime(&t);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tm);
return std::string(buf) + "Z";
};
json result;
result["start_time"] = to_iso(*global_start);
result["end_time"] = to_iso(*global_end);
Logger::Debug("Serving timerange", "CesiumViewer");
return crow::response(result.dump());
});
CROW_ROUTE(app_, "/entities/<path>")
([this](const crow::request&, crow::response& res, std::string entity_path) {
Logger::Debug("Serving entity " + entity_path, "CesiumViewer");
if (entity_path.size() > 5 && entity_path.substr(entity_path.size() - 5) == ".czml") {
std::string entity_id = entity_path.substr(0, entity_path.size() - 5);
std::optional<Entity> entity_opt;
{
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& e : entities_) {
if (e.id == entity_id) {
entity_opt = e;
break;
}
}
}
if (!entity_opt) {
res.code = 404;
res.write("Entity not found");
res.end();
return;
}
std::string czml = CreateCzmlDataForEntity(*entity_opt);
res.set_header("Content-Type", "application/json");
res.write(czml);
res.end();
} else {
res.code = 404;
res.end();
}
});
CROW_ROUTE(app_, "/<path>")
([](const crow::request&, crow::response& res, std::string path) {
Logger::Debug("Serving path " + path, "CesiumViewer");
res.code = 404;
res.end();
});
}
std::string CesiumViewer::CreateCzmlDataForEntity(const Entity& entity) {
auto start = ParseIso8601(entity.initial_time_utc);
double last_time = (entity.times.size() == 0)
? 0.0
: static_cast<double>(entity.times(entity.times.size() - 1));
auto stop = start
+ std::chrono::duration_cast<std::chrono::system_clock::duration>(
std::chrono::duration<double>(last_time));
auto to_iso = [](const std::chrono::system_clock::time_point& tp) {
std::time_t t = std::chrono::system_clock::to_time_t(tp);
std::tm tm = *std::gmtime(&t);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S", &tm);
return std::string(buf) + "Z";
};
std::vector<double> flat_positions;
std::vector<std::string> time_strings;
for (Eigen::Index i = 0; i < entity.times.size(); ++i) {
auto t = start
+ std::chrono::duration_cast<std::chrono::system_clock::duration>(
std::chrono::duration<double>(entity.times(i)));
time_strings.push_back(to_iso(t));
flat_positions.push_back(entity.positions(i, 0) * M_KM);
flat_positions.push_back(entity.positions(i, 1) * M_KM);
flat_positions.push_back(entity.positions(i, 2) * M_KM);
}
// Compose position data as [time, x, y, z, ...]
std::vector<json> position_data;
for (size_t i = 0; i < time_strings.size(); ++i) {
position_data.push_back(time_strings[i]);
position_data.push_back(flat_positions[i * 3 + 0]);
position_data.push_back(flat_positions[i * 3 + 1]);
position_data.push_back(flat_positions[i * 3 + 2]);
}
// Use color with fallback to magenta if not provided
std::vector<int> color = entity.color;
if (color.size() != 3) color = {255, 0, 255};
json czml_entity = {
{"id", entity.id},
{"name", entity.name},
{"description", entity.description},
{"availability", to_iso(start) + "/" + to_iso(stop)},
{"label",
{
{"fillColor", {{"rgba", {255, 255, 255, 255}}}},
{"font", "13pt Lucida Console"},
{"horizontalOrigin", "LEFT"},
{"outlineColor", {{"rgba", {0, 0, 0, 255}}}},
{"outlineWidth", 3},
{"pixelOffset", {{"cartesian2", {20, 0}}}},
{"style", "FILL_AND_OUTLINE"},
{"text", entity.name},
}},
{"position",
{
{"cartesian", position_data},
{"interpolationAlgorithm", "LAGRANGE"},
{"interpolationDegree", 1},
{"referenceFrame", "FIXED"},
}},
{"point",
{
{"pixelSize", entity.size},
{"color", {{"rgba", {color[0], color[1], color[2], 255}}}},
{"outlineColor", {{"rgba", {color[0], color[1], color[2], 255}}}},
{"outlineWidth", 2},
{"show", true},
}},
};
if (entity.times.size() > 1) {
czml_entity["path"] = {
{"show", true},
{"width", 2},
{"material",
{{"solidColor", {{"color", {{"rgba", {color[0], color[1], color[2], 128}}}}}}}},
{"resolution", 120},
};
}
json czml_doc = {
{{"id", "document"}, {"name", "Satellite Visualization"}, {"version", "1.0"}},
czml_entity,
};
return czml_doc.dump(2);
}
// Parse ISO8601 string (YYYY-MM-DDTHH:MM:SSZ) to time_point
std::chrono::system_clock::time_point CesiumViewer::ParseIso8601(const std::string& s) {
std::tm tm = {};
std::istringstream ss(s);
ss >> std::get_time(&tm, "%Y-%m-%dT%H:%M:%S");
if (s.back() == 'Z') {
// UTC
}
return std::chrono::system_clock::from_time_t(std::mktime(&tm));
}
} // namespace lupnt