This commit is contained in:
338
src/storage/map_store.cpp
Normal file
338
src/storage/map_store.cpp
Normal file
@@ -0,0 +1,338 @@
|
||||
#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 {
|
||||
|
||||
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, 11) != SQLITE_NULL)
|
||||
{
|
||||
try
|
||||
{
|
||||
zones = nlohmann::json::parse(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 11)));
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
zones = nlohmann::json::array();
|
||||
}
|
||||
}
|
||||
|
||||
return {{"id", textOrNull(0)},
|
||||
{"name", textOrNull(1)},
|
||||
{"description", textOrNull(2)},
|
||||
{"width", realOrNull(3)},
|
||||
{"height", realOrNull(4)},
|
||||
{"resolution", realOrNull(5)},
|
||||
{"origin_x", realOrNull(6)},
|
||||
{"origin_y", realOrNull(7)},
|
||||
{"origin_yaw", realOrNull(8)},
|
||||
{"image_file", textOrNull(9)},
|
||||
{"yaml_file", textOrNull(10)},
|
||||
{"zones", zones},
|
||||
{"created_at", textOrNull(12)},
|
||||
{"updated_at", textOrNull(13)}};
|
||||
}
|
||||
|
||||
} // 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();
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, description, width, height, resolution, "
|
||||
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
|
||||
"created_at, updated_at FROM maps ORDER BY name",
|
||||
-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_);
|
||||
sqlite3_stmt* stmt = nullptr;
|
||||
if (sqlite3_prepare_v2(db_.handle(),
|
||||
"SELECT id, name, description, width, height, resolution, "
|
||||
"origin_x, origin_y, origin_yaw, image_file, yaml_file, zones_json, "
|
||||
"created_at, updated_at FROM maps WHERE id = ?1",
|
||||
-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 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, 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)",
|
||||
-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 (payload.contains("width") && payload["width"].is_number())
|
||||
sqlite3_bind_double(stmt, 4, payload["width"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 4);
|
||||
if (payload.contains("height") && payload["height"].is_number())
|
||||
sqlite3_bind_double(stmt, 5, payload["height"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 5);
|
||||
if (payload.contains("resolution") && payload["resolution"].is_number())
|
||||
sqlite3_bind_double(stmt, 6, payload["resolution"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 6);
|
||||
sqlite3_bind_double(stmt, 7, payload.value("origin_x", 0.0));
|
||||
sqlite3_bind_double(stmt, 8, payload.value("origin_y", 0.0));
|
||||
sqlite3_bind_double(stmt, 9, payload.value("origin_yaw", 0.0));
|
||||
sqlite3_bind_null(stmt, 10);
|
||||
sqlite3_bind_null(stmt, 11);
|
||||
const std::string zones_str = zones.dump();
|
||||
sqlite3_bind_text(stmt, 12, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 13, now.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 14, 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;
|
||||
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", "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, width=?4, height=?5, resolution=?6, "
|
||||
"origin_x=?7, origin_y=?8, origin_yaw=?9, zones_json=?10, updated_at=?11 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);
|
||||
if (merged["width"].is_number())
|
||||
sqlite3_bind_double(stmt, 4, merged["width"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 4);
|
||||
if (merged["height"].is_number())
|
||||
sqlite3_bind_double(stmt, 5, merged["height"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 5);
|
||||
if (merged["resolution"].is_number())
|
||||
sqlite3_bind_double(stmt, 6, merged["resolution"].get<double>());
|
||||
else
|
||||
sqlite3_bind_null(stmt, 6);
|
||||
sqlite3_bind_double(stmt, 7, merged.value("origin_x", 0.0));
|
||||
sqlite3_bind_double(stmt, 8, merged.value("origin_y", 0.0));
|
||||
sqlite3_bind_double(stmt, 9, merged.value("origin_yaw", 0.0));
|
||||
sqlite3_bind_text(stmt, 10, zones_str.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 11, 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
|
||||
Reference in New Issue
Block a user