// Copyright (c) 2023 INRIA // All rights reserved. // // This file is part of CGAL (www.cgal.org) // // $URL$ // $Id$ // SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial // // Author(s) : Jackson Campolattaro #ifndef CGAL_PROPERTY_CONTAINTER_H #define CGAL_PROPERTY_CONTAINTER_H #include #include #include #include #include #ifndef DOXYGEN_RUNNING namespace CGAL::Properties::Experimental { template class Property_array_base { public: Property_array_base() = default; Property_array_base(const Property_array_base& rhs) = delete; virtual ~Property_array_base() = default; // Declare virtual functions here, for things which need to be done within the Property container // todo: these should mostly be private, and made available using friend virtual std::shared_ptr> empty_clone(const std::vector& active_indices) = 0; virtual std::shared_ptr> clone(const std::vector& active_indices) = 0; virtual void copy(const Property_array_base& other) = 0; // desactived as MSVC 2017 as an issue with that but it is not currently used. #if 0 virtual void move(Property_array_base&& other) = 0; #endif virtual void append(const Property_array_base& other) = 0; virtual void resize(std::size_t n) = 0; virtual void shrink_to_fit() = 0; virtual void swap(Index a, Index b) = 0; virtual void reset(Index i) = 0; virtual const std::type_info& type() const = 0; virtual void transfer_from(const Property_array_base& other_base, Index other_index, Index this_index) = 0; }; /*! * \brief Indexed storage for arbitrary types * * todo: make this effectively private, prioritize the use of Property_array_handle * * @tparam T */ template class Property_array : public Property_array_base { std::vector m_data; const std::vector& m_active_indices; T m_default_value; public: using value_type = T; using reference = typename std::vector::reference; using const_reference = typename std::vector::const_reference; using iterator = typename std::vector::iterator; using const_iterator = typename std::vector::const_iterator; Property_array(const std::vector& active_indices, const T& default_value) : m_data(), m_active_indices(active_indices), m_default_value(default_value) { m_data.resize(active_indices.size(), m_default_value); } virtual std::shared_ptr> empty_clone(const std::vector& active_indices) override { return std::make_shared>(active_indices, m_default_value); } virtual std::shared_ptr> clone(const std::vector& active_indices) override { auto new_array = std::make_shared>(active_indices, m_default_value); new_array->m_data = m_data; return new_array; } virtual void copy(const Property_array_base& other_base) override { auto& other = dynamic_cast&>(other_base); m_data = other.m_data; CGAL_precondition(m_active_indices.size() == m_data.size()); } // deactivated as MSVC 2017 has an issue with that but it is not currently used. #if 0 virtual void move(Property_array_base&& other_base) override { auto&& other = static_cast&&>(other_base); m_data = std::move(other.m_data); CGAL_precondition(m_active_indices.size() == m_data.size()); } #endif virtual void append(const Property_array_base& other_base) override { auto& other = dynamic_cast&>(other_base); CGAL_precondition(m_data.size() + other.m_data.size() == m_active_indices.size()); m_data.insert(m_data.end(), other.m_data.begin(), other.m_data.end()); } virtual void resize(std::size_t n) override { CGAL_precondition(m_active_indices.size() == n); m_data.resize(n, m_default_value); }; virtual void shrink_to_fit() override { m_data.shrink_to_fit(); } virtual void swap(Index a, Index b) override { // todo: maybe cast to index, instead of casting index to size? CGAL_precondition(std::size_t(a) < m_data.size() && std::size_t(b) < m_data.size()); std::iter_swap(m_data.begin() + a, m_data.begin() + b); }; virtual void reset(Index i) override { CGAL_precondition(std::size_t(i) < m_data.size()); m_data[std::size_t(i)] = m_default_value; }; virtual const std::type_info& type() const override { return typeid(T); }; virtual void transfer_from(const Property_array_base& other_base, Index other_index, Index this_index) override { CGAL_precondition(other_base.type() == type()); auto& other = dynamic_cast&>(other_base); CGAL_precondition(std::size_t(other_index) < other.capacity() && std::size_t(this_index) < capacity()); m_data[this_index] = other.m_data[other_index]; } public: // todo: there's not really a good reason to use these, maybe they should be removed [[nodiscard]] std::size_t size() const { return std::count(m_active_indices.begin(), m_active_indices.end(), true); } [[nodiscard]] std::size_t capacity() const { return m_data.size(); } const_reference operator[](Index i) const { CGAL_precondition(std::size_t(i) < m_data.size()); return m_data[std::size_t(i)]; } reference operator[](Index i) { CGAL_precondition(std::size_t(i) < m_data.size()); return m_data[std::size_t(i)]; } iterator begin() { return m_data.begin(); } iterator end() { return m_data.end(); } const_iterator begin() const { return m_data.begin(); } const_iterator end() const { return m_data.end(); } public: bool operator==(const Property_array& other) const { return &other == this; } bool operator!=(const Property_array& other) const { return !operator==(other); } }; // todo: property maps/array handles should go in their own file // todo: add const/read-only handle template class Property_array_handle { std::reference_wrapper> m_array; public: // Necessary for use as a boost::property_type using key_type = Index; using value_type = T; using reference = typename std::vector::reference; using const_reference = typename std::vector::const_reference; using category = boost::lvalue_property_map_tag; using iterator = typename std::vector::iterator; using const_iterator = typename std::vector::const_iterator; Property_array_handle(Property_array& array) : m_array(array) {} [[nodiscard]] std::size_t size() const { return m_array.get().size(); } [[nodiscard]] std::size_t capacity() const { return m_array.get().capacity(); } Property_array& array() const { return m_array.get(); } // todo: This might not be needed, if the other operator[] is made const const_reference operator[](Index i) const { return m_array.get()[i]; } reference operator[](Index i) { return m_array.get()[i]; } // todo: maybe these can be const, in an lvalue property map? iterator begin() { return m_array.get().begin(); } iterator end() { return m_array.get().end(); } const_iterator begin() const { return m_array.get().begin(); } const_iterator end() const { return m_array.get().end(); } bool operator==(const Property_array_handle& other) const { return other.m_array.get() == m_array.get(); } bool operator!=(const Property_array_handle& other) const { return !operator==(other); } inline friend reference get(Property_array_handle p, const Index& i) { return p[i]; } inline friend void put(Property_array_handle p, const Index& i, const T& v) { p[i] = v; } }; template class Property_container { std::multimap>> m_properties; std::vector m_active_indices{}; bool m_has_deleted_elements = false; public: template using Array = Property_array; Property_container() = default; Property_container(const Property_container& other) { m_active_indices = other.m_active_indices; m_has_deleted_elements = other.m_has_deleted_elements; for (auto [name, array] : other.m_properties) { // todo: this could probably be made faster using emplace_hint m_properties.emplace( name, array->clone(m_active_indices) ); } } Property_container(Property_container&& other) { *this = std::move(other); } // This is not exactly an assignment as existing unique properties are kept. Property_container& operator=(const Property_container& other) { m_active_indices = other.m_active_indices; m_has_deleted_elements = other.m_has_deleted_elements; for (auto [name, array] : other.m_properties) { // search if property already exists auto range = m_properties.equal_range(name); auto it = range.first; for (; it != range.second; it++) { if (typeid(*array) == typeid((*it->second))) break; } if (it != range.second) it->second->copy(*array); else m_properties.emplace(name, array->clone(m_active_indices)); } return *this; } // This is not exactly an assignment as existing unique properties are kept. Property_container& operator=(Property_container&& other) { m_active_indices = std::move(other.m_active_indices); m_has_deleted_elements = other.m_has_deleted_elements; for (auto [name, array] : other.m_properties) { // search if property already exists auto range = m_properties.equal_range(name); auto it = range.first; for (; it != range.second; it++) { if (typeid(*array) == typeid((*it->second))) break; } if (it != range.second) it->second->copy(std::move(*array)); else m_properties.emplace(name, array->clone(m_active_indices)); } // The moved-from property map should retain all of its properties, but contain 0 elements other.resize(0); return *this; } template std::pair>, bool> get_or_add_property(const std::string& name, const T default_value = T()) { auto range = m_properties.equal_range(name); for (auto it = range.first; it != range.second; it++) { Property_array* typed_array_ptr = dynamic_cast*>(it->second.get()); if (typed_array_ptr != nullptr) return { {*typed_array_ptr}, false }; } auto it = m_properties.emplace( name, std::make_shared>( m_active_indices, default_value ) ); return {{*dynamic_cast*>(it->second.get())}, true}; } template Property_array& add_property(const std::string& name, const T default_value = T()) { // todo: I'm not settled on the naming, but it's really convenient to have a function like this auto [array, created] = get_or_add_property(name, default_value); CGAL_precondition(created); return array.get(); } /* // todo: misleading name, maybe it could be add_same_properties? void copy_properties(const Property_container& other) { for (auto [name, other_array]: other.m_properties) { // If this container doesn't have any property by this name, add it (with the same type as in other) if (!property_exists(name)) m_property_arrays.emplace(name, other_array->empty_clone(m_active_indices)); } }*/ template const Property_array& get_property(const std::string& name) const { return *(get_property_if_exists(name)); } template Property_array& get_property(const std::string& name) { return *(get_property_if_exists(name)); } template std::optional>> get_property_if_exists(const std::string& name) const { auto range = m_properties.equal_range(name); for (auto it = range.first; it != range.second; it++) { Property_array* typed_array_ptr = dynamic_cast*>(it->second.get()); if (typed_array_ptr != nullptr) return *typed_array_ptr; } return {}; } template bool property_exists(const std::string& name) const { auto range = m_properties.equal_range(name); for (auto it = range.first; it != range.second; it++) { Property_array* typed_array_ptr = dynamic_cast*>(it->second.get()); if (typed_array_ptr != nullptr) return true; } return false; } /*! * Removes all properties with the name from the container. * * @param name * @return number of removed properties. */ std::size_t remove_properties(const std::string& name) { return m_properties.erase(name); } template bool remove_property(const Property_array& arrayToRemove) { const Property_array_base* ref = dynamic_cast*>(&arrayToRemove); for (auto it = m_properties.begin(); it != m_properties.end(); it++) { auto const& [name, array] = *it; if (array.get() == ref) { m_properties.erase(it); return true; } } return false; } void remove_all_properties_except(const std::vector& preserved_names) { // todo: if this is used often, it should take a parameter pack instead of a vector // A fold expression could then be used in place of std::find for better performance for (auto it = m_properties.begin(); it != m_properties.end();) { auto const& [name, array] = *it; if (std::find(preserved_names.begin(), preserved_names.end(), name) == preserved_names.end()) it = m_properties.erase(it); else it++; } } std::vector properties() const { std::vector property_names{}; for (auto const& [name, _]: m_properties) property_names.emplace_back(name); return property_names; } std::size_t num_properties() const { return m_properties.size(); } /* Deactivated as there may be several Property_maps with different types but the same name. const std::type_info& property_type(const std::string& name) const { if (auto it = m_property_arrays.find(name); it != m_property_arrays.end()) return it->second->type(); else return typeid(void); }*/ public: void resize(std::size_t n) { m_active_indices.resize(n); for (auto [name, array] : m_properties) array->resize(n); std::fill(m_active_indices.begin(), m_active_indices.end(), true); m_has_deleted_elements = false; } [[nodiscard]] std::size_t size() const { return std::count(m_active_indices.begin(), m_active_indices.end(), true); } [[nodiscard]] std::size_t capacity() const { return m_active_indices.size(); } Index emplace_back() { // Expand the storage and return the last element m_active_indices.push_back(true); for (auto [name, array] : m_properties) array->resize(capacity()); auto first_new_index = Index(capacity() - 1); reset(first_new_index); return first_new_index; } Index emplace() { if (!m_has_deleted_elements) return emplace_back(); // If there are empty slots, return the index of one of them and mark it as full auto first_unused = std::find_if(m_active_indices.begin(), m_active_indices.end(), [](bool used) { return !used; }); if (first_unused != m_active_indices.end()) { *first_unused = true; auto index = Index(std::distance(m_active_indices.begin(), first_unused)); reset(index); return index; } m_has_deleted_elements = false; return emplace_back(); } Index emplace_group_back(std::size_t n) { // Expand the storage and return the start of the new region m_active_indices.resize(capacity() + n, true); for (auto [name, array] : m_properties) array->resize(capacity()); return Index(capacity() - n); } Index emplace_group(std::size_t n) { if (!m_has_deleted_elements) return emplace_group_back(n); auto search_start = m_active_indices.begin(); while (search_start != m_active_indices.end()) { // Find the first unused cell auto unused_begin = std::find_if( search_start, m_active_indices.end(), [](bool used) { return !used; } ); auto unused_end = unused_begin; // Determine if the group fits if (std::distance(unused_begin, m_active_indices.end()) >= static_cast::iterator>::difference_type>(n)) unused_end = std::find_if( unused_begin, (std::min)(unused_begin + n, m_active_indices.end()), [](bool used) { return used; } ); // If the discovered range was large enough if (std::distance(unused_begin, unused_end) >= static_cast::iterator>::difference_type>(n)) { // Mark the indices as used, and reset the properties of each of them // todo: it would be better to provide a function to set a range for (auto it = unused_begin; it < unused_end; ++it) { *it = true; reset(Index(std::distance(m_active_indices.begin(), it))); } // Return the first index of the range return Index(std::distance(m_active_indices.begin(), unused_begin)); } // If we didn't find a large enough region, continue our search after the end search_start = unused_end; } // If no empty regions were found, expand the storage return emplace_group_back(n); } void swap(Index a, Index b) { for (auto [name, array]: m_properties) array->swap(a, b); } void reset(Index i) { for (auto [name, array]: m_properties) array->reset(i); } void erase(Index i) { m_active_indices[i] = false; m_has_deleted_elements = true; for (auto [name, array]: m_properties) array->reset(i); } bool is_erased(Index i) const { return !m_active_indices[i]; } // todo: I'd prefer to eliminate this, if possible void mark_active(Index i) { return m_active_indices[i] = true; } void mark_inactive(Index i) { return m_active_indices[i] = false; } std::vector active_list() const { std::vector indices; for (std::size_t i = 0; i < m_active_indices.size(); ++i) if (m_active_indices[i]) indices.emplace_back(i); return indices; } std::vector inactive_list() const { std::vector indices; for (std::size_t i = 0; i < m_active_indices.size(); ++i) if (!m_active_indices[i]) indices.emplace_back(i); return indices; } void shrink_to_fit() { for (auto [name, array]: m_properties) array->shrink_to_fit(); } /*! * Adds the elements of the other container to this container for each property which is present in this container. * * Gaps in both containers are preserved, and all elements of the other container are guaranteed * to appear after the elements of this container. * Properties in this container which don't appear in the other container are extended with default values. * Properties in the other container which don't appear in this one are not included. * todo: merge() would be useful as well, but could break contiguous regions in the other container * * @param other */ void append(const Property_container& other) { m_active_indices.insert(m_active_indices.end(), other.m_active_indices.begin(), other.m_active_indices.end()); for (auto [name, array]: m_properties) { auto range = other.m_properties.equal_range(name); auto it = range.first; for (; it != range.second; it++) { if (typeid(array.get()) == typeid((it->second.get()))) break; } if (it != range.second) array->append(*it->second.get()); else array->resize(m_active_indices.size()); } } /* // todo: maybe should be renamed to transfer_from, but I'd rather remove this functionality entirely void transfer(const Property_container& other, Index other_index, Index this_index) { CGAL_precondition(other.m_property_arrays.size() == m_property_arrays.size()); for (auto [name, array]: m_property_arrays) { auto other_array = other.m_property_arrays.at(name); array->transfer_from(*other_array, other_index, this_index); } }*/ // todo: maybe a compress() method? }; } #endif // DOXYGEN_RUNNING #endif //CGAL_PROPERTY_CONTAINTER_H