353 lines
12 KiB
C++
353 lines
12 KiB
C++
#include "storage/map_store.hpp"
|
|
|
|
#include "storage/database.hpp"
|
|
#include "util/file_util.hpp"
|
|
#include "util/id_util.hpp"
|
|
#include "util/string_util.hpp"
|
|
|
|
#include <sqlite3.h>
|
|
|
|
namespace lm {
|
|
|
|
namespace {
|
|
|
|
constexpr const char* kMapSelect =
|
|
"SELECT id, name, description, site_id, created_by, width, height, resolution, "
|
|
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, created_at, updated_at "
|
|
"FROM maps";
|
|
|
|
nlohmann::json rowToJson(sqlite3_stmt* stmt)
|
|
{
|
|
auto textOrNull = [&](int col) -> nlohmann::json {
|
|
if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
|
|
return nullptr;
|
|
return nlohmann::json(reinterpret_cast<const char*>(sqlite3_column_text(stmt, col)));
|
|
};
|
|
auto realOrNull = [&](int col) -> nlohmann::json {
|
|
if (sqlite3_column_type(stmt, col) == SQLITE_NULL)
|
|
return nullptr;
|
|
return nlohmann::json(sqlite3_column_double(stmt, col));
|
|
};
|
|
|
|
nlohmann::json zones = nlohmann::json::array();
|
|
if (sqlite3_column_type(stmt, 13) != SQLITE_NULL)
|
|
{
|
|
try
|
|
{
|
|
zones = nlohmann::json::parse(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 13)));
|
|
}
|
|
catch (...)
|
|
{
|
|
zones = nlohmann::json::array();
|
|
}
|
|
}
|
|
|
|
return {{"id", textOrNull(0)},
|
|
{"name", textOrNull(1)},
|
|
{"description", textOrNull(2)},
|
|
{"site_id", textOrNull(3)},
|
|
{"created_by", textOrNull(4)},
|
|
{"width", realOrNull(5)},
|
|
{"height", realOrNull(6)},
|
|
{"resolution", realOrNull(7)},
|
|
{"origin_x", realOrNull(8)},
|
|
{"origin_y", realOrNull(9)},
|
|
{"origin_yaw", realOrNull(10)},
|
|
{"image_file", textOrNull(11)},
|
|
{"yaml_file", textOrNull(12)},
|
|
{"zones", zones},
|
|
{"created_at", textOrNull(14)},
|
|
{"updated_at", textOrNull(15)}};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MapStore::MapStore(Database& db) : db_(db) {}
|
|
|
|
std::filesystem::path MapStore::mapDir(const std::string& id) const
|
|
{
|
|
return db_.mapsDir() / id;
|
|
}
|
|
|
|
nlohmann::json MapStore::list() const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
nlohmann::json maps = nlohmann::json::array();
|
|
std::string sql = std::string(kMapSelect) + " ORDER BY name";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
|
return maps;
|
|
while (sqlite3_step(stmt) == SQLITE_ROW)
|
|
maps.push_back(rowToJson(stmt));
|
|
sqlite3_finalize(stmt);
|
|
return maps;
|
|
}
|
|
|
|
std::optional<nlohmann::json> MapStore::find(const std::string& id) const
|
|
{
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
std::string sql = std::string(kMapSelect) + " WHERE id = ?1";
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(), sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK)
|
|
return std::nullopt;
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
std::optional<nlohmann::json> out;
|
|
if (sqlite3_step(stmt) == SQLITE_ROW)
|
|
out = rowToJson(stmt);
|
|
sqlite3_finalize(stmt);
|
|
return out;
|
|
}
|
|
|
|
std::optional<nlohmann::json> MapStore::create(const nlohmann::json& payload, std::string& err)
|
|
{
|
|
if (!payload.is_object())
|
|
{
|
|
err = "payload must be an object";
|
|
return std::nullopt;
|
|
}
|
|
const std::string name = StringUtil::trimCopy(payload.value("name", ""));
|
|
if (name.empty())
|
|
{
|
|
err = "name is required";
|
|
return std::nullopt;
|
|
}
|
|
|
|
const std::string id = payload.value("id", IdUtil::newId());
|
|
const std::string now = IdUtil::nowIso8601();
|
|
const std::string description = payload.value("description", "");
|
|
const std::string site_id = payload.value("site_id", "");
|
|
const std::string created_by = payload.value("created_by", "");
|
|
const auto zones = payload.contains("zones") ? payload["zones"] : nlohmann::json::array();
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(mapDir(id), ec);
|
|
if (ec)
|
|
{
|
|
err = "failed to create map directory";
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(),
|
|
"INSERT INTO maps(id, name, description, site_id, created_by, width, height, resolution, "
|
|
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
|
|
"created_at, updated_at) "
|
|
"VALUES(?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16)",
|
|
-1,
|
|
&stmt,
|
|
nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return std::nullopt;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, description.c_str(), -1, SQLITE_TRANSIENT);
|
|
if (site_id.empty())
|
|
sqlite3_bind_null(stmt, 4);
|
|
else
|
|
sqlite3_bind_text(stmt, 4, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 5, created_by.c_str(), -1, SQLITE_TRANSIENT);
|
|
if (payload.contains("width") && payload["width"].is_number())
|
|
sqlite3_bind_double(stmt, 6, payload["width"].get<double>());
|
|
else
|
|
sqlite3_bind_null(stmt, 6);
|
|
if (payload.contains("height") && payload["height"].is_number())
|
|
sqlite3_bind_double(stmt, 7, payload["height"].get<double>());
|
|
else
|
|
sqlite3_bind_null(stmt, 7);
|
|
if (payload.contains("resolution") && payload["resolution"].is_number())
|
|
sqlite3_bind_double(stmt, 8, payload["resolution"].get<double>());
|
|
else
|
|
sqlite3_bind_null(stmt, 8);
|
|
sqlite3_bind_double(stmt, 9, payload.value("origin_x", 0.0));
|
|
sqlite3_bind_double(stmt, 10, payload.value("origin_y", 0.0));
|
|
sqlite3_bind_double(stmt, 11, payload.value("origin_yaw", 0.0));
|
|
sqlite3_bind_null(stmt, 12);
|
|
sqlite3_bind_null(stmt, 13);
|
|
const std::string zones_str = zones.dump();
|
|
sqlite3_bind_text(stmt, 14, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 15, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 16, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
if (sqlite3_step(stmt) != SQLITE_DONE)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
sqlite3_finalize(stmt);
|
|
return std::nullopt;
|
|
}
|
|
sqlite3_finalize(stmt);
|
|
|
|
nlohmann::json created;
|
|
created["id"] = id;
|
|
created["name"] = name;
|
|
created["description"] = description;
|
|
created["site_id"] = site_id.empty() ? nullptr : nlohmann::json(site_id);
|
|
created["created_by"] = created_by;
|
|
if (payload.contains("width") && payload["width"].is_number())
|
|
created["width"] = payload["width"];
|
|
if (payload.contains("height") && payload["height"].is_number())
|
|
created["height"] = payload["height"];
|
|
if (payload.contains("resolution") && payload["resolution"].is_number())
|
|
created["resolution"] = payload["resolution"];
|
|
created["origin_x"] = payload.value("origin_x", 0.0);
|
|
created["origin_y"] = payload.value("origin_y", 0.0);
|
|
created["origin_yaw"] = payload.value("origin_yaw", 0.0);
|
|
created["image_file"] = nullptr;
|
|
created["yaml_file"] = nullptr;
|
|
created["zones"] = zones;
|
|
created["created_at"] = now;
|
|
created["updated_at"] = now;
|
|
return created;
|
|
}
|
|
|
|
bool MapStore::update(const std::string& id, const nlohmann::json& payload, std::string& err)
|
|
{
|
|
auto existing = find(id);
|
|
if (!existing)
|
|
{
|
|
err = "map not found";
|
|
return false;
|
|
}
|
|
|
|
nlohmann::json merged = *existing;
|
|
for (const char* key :
|
|
{"name", "description", "site_id", "created_by", "width", "height", "resolution", "origin_x", "origin_y", "origin_yaw"})
|
|
{
|
|
if (payload.contains(key))
|
|
merged[key] = payload[key];
|
|
}
|
|
if (payload.contains("zones"))
|
|
merged["zones"] = payload["zones"];
|
|
|
|
const std::string now = IdUtil::nowIso8601();
|
|
const std::string zones_str = merged.value("zones", nlohmann::json::array()).dump();
|
|
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(),
|
|
"UPDATE maps SET name=?2, description=?3, site_id=?4, created_by=?5, width=?6, height=?7, "
|
|
"resolution=?8, origin_x=?9, origin_y=?10, origin_yaw=?11, zones_json=?12, updated_at=?13 "
|
|
"WHERE id=?1",
|
|
-1,
|
|
&stmt,
|
|
nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return false;
|
|
}
|
|
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, merged.value("name", "").c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, merged.value("description", "").c_str(), -1, SQLITE_TRANSIENT);
|
|
const std::string site_id = merged.value("site_id", "");
|
|
if (site_id.empty())
|
|
sqlite3_bind_null(stmt, 4);
|
|
else
|
|
sqlite3_bind_text(stmt, 4, site_id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 5, merged.value("created_by", "").c_str(), -1, SQLITE_TRANSIENT);
|
|
if (merged["width"].is_number())
|
|
sqlite3_bind_double(stmt, 6, merged["width"].get<double>());
|
|
else
|
|
sqlite3_bind_null(stmt, 6);
|
|
if (merged["height"].is_number())
|
|
sqlite3_bind_double(stmt, 7, merged["height"].get<double>());
|
|
else
|
|
sqlite3_bind_null(stmt, 7);
|
|
if (merged["resolution"].is_number())
|
|
sqlite3_bind_double(stmt, 8, merged["resolution"].get<double>());
|
|
else
|
|
sqlite3_bind_null(stmt, 8);
|
|
sqlite3_bind_double(stmt, 9, merged.value("origin_x", 0.0));
|
|
sqlite3_bind_double(stmt, 10, merged.value("origin_y", 0.0));
|
|
sqlite3_bind_double(stmt, 11, merged.value("origin_yaw", 0.0));
|
|
sqlite3_bind_text(stmt, 12, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 13, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
|
|
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
|
if (!ok)
|
|
err = sqlite3_errmsg(db_.handle());
|
|
sqlite3_finalize(stmt);
|
|
return ok;
|
|
}
|
|
|
|
bool MapStore::remove(const std::string& id, std::string& err)
|
|
{
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(), "DELETE FROM maps WHERE id = ?1", -1, &stmt, nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return false;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
|
sqlite3_finalize(stmt);
|
|
if (!ok)
|
|
{
|
|
err = "map not found";
|
|
return false;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::remove_all(mapDir(id), ec);
|
|
return true;
|
|
}
|
|
|
|
std::optional<std::filesystem::path> MapStore::imagePath(const std::string& id) const
|
|
{
|
|
const auto map = find(id);
|
|
if (!map || !(*map)["image_file"].is_string())
|
|
return std::nullopt;
|
|
const auto path = mapDir(id) / map->value("image_file", "");
|
|
if (!std::filesystem::exists(path))
|
|
return std::nullopt;
|
|
return path;
|
|
}
|
|
|
|
bool MapStore::saveImageFile(const std::string& id,
|
|
const std::string& filename,
|
|
const std::string& bytes,
|
|
std::string& err)
|
|
{
|
|
if (!find(id))
|
|
{
|
|
err = "map not found";
|
|
return false;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(mapDir(id), ec);
|
|
const auto path = mapDir(id) / filename;
|
|
if (!FileUtil::writeBinaryAtomic(path, bytes))
|
|
{
|
|
err = "failed to write image file";
|
|
return false;
|
|
}
|
|
|
|
const std::string now = IdUtil::nowIso8601();
|
|
std::lock_guard<std::mutex> lock(mu_);
|
|
sqlite3_stmt* stmt = nullptr;
|
|
if (sqlite3_prepare_v2(db_.handle(),
|
|
"UPDATE maps SET image_file = ?2, updated_at = ?3 WHERE id = ?1",
|
|
-1,
|
|
&stmt,
|
|
nullptr) != SQLITE_OK)
|
|
{
|
|
err = sqlite3_errmsg(db_.handle());
|
|
return false;
|
|
}
|
|
sqlite3_bind_text(stmt, 1, id.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 2, filename.c_str(), -1, SQLITE_TRANSIENT);
|
|
sqlite3_bind_text(stmt, 3, now.c_str(), -1, SQLITE_TRANSIENT);
|
|
const bool ok = sqlite3_step(stmt) == SQLITE_DONE;
|
|
if (!ok)
|
|
err = sqlite3_errmsg(db_.handle());
|
|
sqlite3_finalize(stmt);
|
|
return ok;
|
|
}
|
|
|
|
} // namespace lm
|