#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 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(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(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 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 MapStore::find(const std::string& id) const { std::lock_guard 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 out; if (sqlite3_step(stmt) == SQLITE_ROW) out = rowToJson(stmt); sqlite3_finalize(stmt); return out; } std::optional 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 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()); else sqlite3_bind_null(stmt, 6); if (payload.contains("height") && payload["height"].is_number()) sqlite3_bind_double(stmt, 7, payload["height"].get()); else sqlite3_bind_null(stmt, 7); if (payload.contains("resolution") && payload["resolution"].is_number()) sqlite3_bind_double(stmt, 8, payload["resolution"].get()); 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 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()); else sqlite3_bind_null(stmt, 6); if (merged["height"].is_number()) sqlite3_bind_double(stmt, 7, merged["height"].get()); else sqlite3_bind_null(stmt, 7); if (merged["resolution"].is_number()) sqlite3_bind_double(stmt, 8, merged["resolution"].get()); 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 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 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 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