#include "common.hh" #include "config.hh" #include "files_finder.hh" #include "image.hh" #include "logger.hh" #include "rotation.hh" #include "strutil.hh" #include "task_runner.hh" #include "timezone.hh" #include "travel.hh" #include "video.hh" #include "weak_ptr.hh" #include #include #include #include #include #include #include namespace { class TravelImpl : public Travel, public FilesFinder::Delegate { public: TravelImpl(std::shared_ptr logger, std::shared_ptr runner) : logger_(std::move(logger)), runner_(std::move(runner)), weak_ptr_owner_(this) {} bool setup(Logger* logger, Config* config) override { root_ = config->get_path("site.root", ""); if (root_.empty()) { logger->err("site.root must be set to the root directory for travels."); return false; } auto travel_files_threads = config->get("workers", 4); if (!travel_files_threads.has_value()) { logger->err("workers is unknown value: '%s'", config->get("workers", nullptr)); return false; } if (travel_files_threads.value() <= 0) { logger->err("workers must be > 0"); return false; } timezone_ = Timezone::create(logger_, config->get_path("geojson.database", ""), config->get_path("zoneinfo.root", "/usr/share/zoneinfo")); worker_threads_ = travel_files_threads.value(); return true; } void start() override { workers_ = TaskRunner::create(worker_threads_); finder_ = FilesFinder::create(logger_, workers_, root_, this, worker_threads_); } void reload() override { // Make implementation simpler by ignoring calls to reload() while // previoys start() or reload() is still in progress. if (finder_) { logger_->warn("Reload called while still loading, ignored."); return; } workers_.reset(); trips_.clear(); trip_index_.clear(); start(); } size_t trips() const override { return trips_.size(); } Trip const& trip(size_t i) const override { assert(i < trips_.size()); return trips_[i]; } void call_when_loaded(std::function callback) override { if (finder_) { call_when_loaded_.push_back(std::move(callback)); } else { callback(); } } // Called on any thread. bool include_dir(std::string_view name, uint16_t depth) const override { if (depth > 0) { if (name == "backup") return false; return FilesFinder::Delegate::include_dir(name, depth); } return valid_trip_id(name); } // Runs on workers void file(std::filesystem::path path) override { std::string media_id; std::string trip_id = get_trip_id(path, &media_id); if (trip_id.empty()) { logger_->warn("Ignoring %s because no trip id found", path.c_str()); return; } auto image = get_image_info(media_id, path); if (image) { if (image.value().date_.empty()) { logger_->dbg("Ignoring %s, image without a date.", path.c_str()); return; } runner_->post(std::bind(&TravelImpl::weak_finder_image, weak_ptr_owner_.get(), trip_id, image.value())); return; } auto video = get_video_info(media_id, path, timezone_.get()); if (video) { if (video.value().date_.empty()) { logger_->dbg("Ignoring %s, video without a date.", path.c_str()); return; } runner_->post(std::bind(&TravelImpl::weak_finder_video, weak_ptr_owner_.get(), trip_id, video.value())); return; } logger_->dbg("Ignoring %s, not an image or a video.", path.c_str()); } // Runs on workers void done() override { runner_->post(std::bind(&TravelImpl::weak_finder_done, weak_ptr_owner_.get())); } private: struct Info { std::string id_; std::filesystem::path path_; uint64_t width_; uint64_t height_; Location location_; Date date_; Info(std::string id, std::filesystem::path path, uint64_t width, uint64_t height, Location location, Date date) : id_(std::move(id)), path_(std::move(path)), width_(width), height_(height), location_(location), date_(date) {} }; struct ImageInfo : Info { Rotation rotation_; std::string thumbnail_mime_type_; uint64_t thumbnail_size_; ImageInfo(std::string id, std::filesystem::path path, uint64_t width, uint64_t height, Location location, Date date, Rotation rotation, std::string thumbnail_mime_type, uint64_t thumbnail_size) : Info(std::move(id), std::move(path), width, height, location, date), rotation_(rotation), thumbnail_mime_type_(std::move(thumbnail_mime_type)), thumbnail_size_(thumbnail_size) {} }; struct VideoInfo : Info { double length_; VideoInfo(std::string id, std::filesystem::path path, uint64_t width, uint64_t height, Location location, Date date, double length) : Info(std::move(id), std::move(path), width, height, location, date), length_(length) {} }; class MediaImpl : public virtual Media, public virtual Thumbnail { public: explicit MediaImpl(ImageInfo info) : image_(std::move(info)) {} explicit MediaImpl(VideoInfo info) : video_(std::move(info)) {} std::string_view id() const override { return image_ ? image_->id_ : video_->id_; } std::filesystem::path const& path() const override { return image_ ? image_->path_ : video_->path_; } Type type() const override { return image_ ? Type::IMAGE : Type::VIDEO; } uint64_t width() const override { return image_ ? image_->width_ : video_->width_; } uint64_t height() const override { return image_ ? image_->height_ : video_->height_; } Location location() const override { return image_ ? image_->location_ : video_->location_; } Date date() const override { return image_ ? image_->date_ : video_->date_; } double length() const override { return image_ ? 0.0 : video_->length_; } Rotation rotation() const override { return image_ ? image_->rotation_ : Rotation::UNKNOWN; } Thumbnail const* thumbnail() const override { return image_ && !image_->thumbnail_mime_type_.empty() && image_->thumbnail_size_ > 0 ? this : nullptr; } ThumbType thumb_type() const override { return ThumbType::EXIF; } std::string_view mime_type() const override { return image_->thumbnail_mime_type_; } uint64_t size() const override { return image_->thumbnail_size_; } private: std::optional image_; std::optional video_; }; class DayImpl : public Day { public: DayImpl(Date day, size_t first) : day_(day), first_(first), last_(first) { } Date date() const override { return day_; } size_t first() const override { return first_; } size_t last() const override { return last_; } void increment_last() { ++last_; } private: Date const day_; size_t const first_; size_t last_; }; class TripImpl : public Trip { public: TripImpl(std::string id, std::string name, uint16_t year) : id_(std::move(id)), name_(std::move(name)), year_(year) {} std::string_view id() const override { return id_; } std::string_view title() const override { return name_; } uint16_t year() const override { return year_; } Location location() const override { return location_; } uint64_t images() const override { return images_; } uint64_t videos() const override { return videos_; } size_t media_count() const override { return media_.size(); } Media const& media(size_t i) const override { assert(i < media_.size()); return media_[i]; } size_t day_count() const override { return day_.size(); } Day const& day(size_t i) const override { assert(i < day_.size()); return day_[i]; } void add_image(ImageInfo info) { ++images_; media_.emplace_back(std::move(info)); } void add_video(VideoInfo info) { ++videos_; media_.emplace_back(std::move(info)); } void sort_media() { std::sort(media_.begin(), media_.end(), [] (MediaImpl const& a, MediaImpl const& b) { return a.date() < b.date(); }); } void setup_days() { for (size_t i = 0; i < media_.size(); ++i) { auto day = media_[i].date().day(); if (day_.empty() || day != day_.back().date()) { assert(day_.empty() || day > day_.back().date()); day_.emplace_back(day, i); } else { assert(i == day_.back().last() + 1); day_.back().increment_last(); } } } void set_location(Location location) { location_ = location; } private: std::string id_; std::string name_; uint16_t year_; Location location_; uint64_t images_{0}; uint64_t videos_{0}; std::vector media_; std::vector day_; }; // Called from workers std::string get_trip_id(std::filesystem::path const& path, std::string* out_child_id) const { std::string child_id; std::filesystem::path tmp = path; while (true) { if (!tmp.has_parent_path()) break; auto parent = tmp.parent_path(); if (parent == root_) { if (out_child_id) *out_child_id = std::move(child_id); return tmp.filename(); } if (!child_id.empty()) child_id.insert(0, "/"); child_id.insert(0, tmp.filename()); tmp = parent; } return std::string(); } static void weak_finder_image(std::shared_ptr> weak_ptr, std::string const& trip_id, ImageInfo const& info) { auto* ptr = weak_ptr->get(); if (ptr) ptr->finder_image(trip_id, info); } static void weak_finder_video(std::shared_ptr> weak_ptr, std::string const& trip_id, VideoInfo const& info) { auto* ptr = weak_ptr->get(); if (ptr) ptr->finder_video(trip_id, info); } static void weak_finder_done(std::shared_ptr> weak_ptr) { auto* ptr = weak_ptr->get(); if (ptr) ptr->finder_done(); } TripImpl* get_trip(std::string const& trip_id) { auto it = trip_index_.find(trip_id); if (it != trip_index_.end()) return &trips_[it->second]; std::string name; uint16_t year; if (parse_trip_id(trip_id, &name, &year)) { auto index = trips_.size(); trips_.emplace_back(trip_id, std::move(name), year); trip_index_[trip_id] = index; return &trips_[index]; } else { // include_dir should make sure of this doesn't happen. assert(false); return nullptr; } } void finder_image(std::string const& trip_id, ImageInfo const& info) { auto* trip_ptr = get_trip(trip_id); if (trip_ptr) trip_ptr->add_image(info); } void finder_video(std::string const& trip_id, VideoInfo const& info) { auto* trip_ptr = get_trip(trip_id); if (trip_ptr) trip_ptr->add_video(info); } void finder_done() { finder_.reset(); for (auto& trip_impl : trips_) cleanup_trip(trip_impl); logger_->info("Trips all loaded."); for (auto& callback : call_when_loaded_) { callback(); } call_when_loaded_.clear(); } static Location get_mean_location(std::vector locations) { if (locations.empty()) return Location(); auto it = locations.begin(); Location mean = *it; for (++it; it != locations.end(); ++it) { mean.lat += it->lat; mean.lng += it->lng; } mean.lat /= locations.size(); mean.lng /= locations.size(); return mean; } static Location get_center_location(std::vector locations) { while (true) { if (locations.empty()) return Location(); Location mean = get_mean_location(locations); float variance = 0; std::vector location_z; for (auto const& loc : locations) { float deviation_square = std::pow(loc.lat - mean.lat, 2.f) + std::pow(loc.lng - mean.lng, 2.f); variance += deviation_square; location_z.push_back(std::sqrt(deviation_square)); } float standard_deviation = std::sqrt(variance / locations.size()); for (auto& z : location_z) z /= standard_deviation; // Remove outliers (if any) bool removed_any = false; size_t i = 0; while (i < locations.size()) { if (std::abs(location_z[i]) > 1.43f) { locations.erase(locations.begin() + i); location_z.erase(location_z.begin() + i); removed_any = true; } else { ++i; } } if (!removed_any) return mean; } } void cleanup_trip(TripImpl& trip_impl) { // Sort by date trip_impl.sort_media(); trip_impl.setup_days(); std::vector locations; for (size_t i = 0; i < trip_impl.media_count(); ++i) { auto& media = trip_impl.media(i); if (media.location().empty()) continue; locations.push_back(media.location()); } trip_impl.set_location(get_center_location(std::move(locations))); } static std::optional get_image_info(std::string id, std::filesystem::path path) { auto image = Image::load(path); if (image) { auto* thumb = image->thumbnail(); return ImageInfo(std::move(id), std::move(path), image->width(), image->height(), image->location(), image->date(), image->rotation(), thumb ? std::string(thumb->mime_type()) : std::string(), thumb ? thumb->size() : 0); } return std::nullopt; } static std::optional get_video_info(std::string id, std::filesystem::path path, Timezone const* timezone) { auto video = Video::load(path, timezone); if (video) return VideoInfo(std::move(id), std::move(path), video->width(), video->height(), video->location(), video->date(), video->length()); return std::nullopt; } static bool valid_trip_id(std::string_view id) { return parse_trip_id(id, nullptr, nullptr); } static bool parse_trip_id(std::string_view id, std::string* name, uint16_t* year) { auto it = id.find('-'); if (it == std::string_view::npos) return false; if (name) name->assign(str::trim(id.substr(0, it))); auto tmp = str::parse_uint16(std::string(id, it + 1, std::string::npos)); if (!tmp) return false; if (*tmp < 1950) return false; if (year) *year = *tmp; return true; } std::shared_ptr logger_; std::shared_ptr runner_; std::unique_ptr timezone_; size_t worker_threads_{0}; std::filesystem::path root_; std::vector trips_; std::unordered_map trip_index_; std::vector> call_when_loaded_; std::mutex worker_mutex_; // It is important that workers_ is (next to) last as it blocks leftover // workers in destructor so should be destroyed first. std::shared_ptr workers_; // finder depends on workers so must be destroyed before. std::unique_ptr finder_; WeakPtrOwner weak_ptr_owner_; }; } // namespace std::unique_ptr Travel::create(std::shared_ptr logger, std::shared_ptr runner) { return std::make_unique(std::move(logger), std::move(runner)); }