From f8d786958a3ddc84be2750da1e70901aa699614d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verin=20Lemaignan?= Date: Wed, 10 Sep 2025 00:08:16 +0200 Subject: [PATCH] [generator] retrieve socket:* OSM tags used by amenity:charging_station MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently support the following socket types: - type 1 - type 1 combo - type 2 (wired or wo/ cable) - type 2 combo - chademo - nacs This commit also adds initial display of the socket types and power the to Qt desktop app. Signed-off-by: Séverin Lemaignan --- .../sdk/bookmarks/data/Metadata.java | 4 +- generator/osm2meta.cpp | 195 +++++++++++++++++- generator/osm2meta.hpp | 32 +++ libs/indexer/feature_meta.cpp | 3 + libs/indexer/feature_meta.hpp | 1 + libs/indexer/map_object.cpp | 56 +++++ libs/indexer/map_object.hpp | 21 ++ qt/place_page_dialog_user.cpp | 20 ++ 8 files changed, 330 insertions(+), 2 deletions(-) diff --git a/android/sdk/src/main/java/app/organicmaps/sdk/bookmarks/data/Metadata.java b/android/sdk/src/main/java/app/organicmaps/sdk/bookmarks/data/Metadata.java index 84d607478..509df74a9 100644 --- a/android/sdk/src/main/java/app/organicmaps/sdk/bookmarks/data/Metadata.java +++ b/android/sdk/src/main/java/app/organicmaps/sdk/bookmarks/data/Metadata.java @@ -70,7 +70,9 @@ public class Metadata implements Parcelable FMD_CONTACT_BLUESKY(51), FMD_PANORAMAX(52), FMD_CHECK_DATE(53), - FMD_CHECK_DATE_OPEN_HOURS(54); + FMD_CHECK_DATE_OPEN_HOURS(54), + //FMD_BRANCH(55), + FMD_CHARGE_SOCKETS(56); private final int mMetaType; MetadataType(int metadataType) diff --git a/generator/osm2meta.cpp b/generator/osm2meta.cpp index ed2023e51..a478cd6bf 100644 --- a/generator/osm2meta.cpp +++ b/generator/osm2meta.cpp @@ -19,6 +19,7 @@ #include #include #include +#include namespace { @@ -81,9 +82,193 @@ bool Prefix2Double(std::string const & str, double & d) d = std::strtod(s, &stop); return (s != stop && math::is_finite(d)); } - } // namespace +void MetadataTagProcessorImpl::AggregateChargeSocket(std::string const & k, std::string const & v) +{ + auto keys = strings::Tokenize(k, ":"); + ASSERT(keys[0] == "socket", ()) // key must start with "socket:" + if (keys.size() < 2 || keys.size() > 3) + { + LOG(LWARNING, ("Invalid socket key:", k)); + return; + } + + std::string type(keys[1]); + + bool isOutput = false; + if (keys.size() == 3) + { + if (keys[2] == "output") + isOutput = true; + else + return; // ignore other suffixes + } + + // normalize type if needed + // based on recommandations from https://wiki.openstreetmap.org/wiki/Key:socket:* + static std::unordered_map const kTypeMap = { + {"tesla_supercharger", "nacs"}, // also used in EU for 'type2_combo' -> needs fix in OSM tagging + {"tesla_destination", "nacs"}, + {"tesla_standard", "nacs"}, + {"tesla", "nacs"}, + {"tesla_supercharger_ccs", "type2_combo"}, + {"ccs", "type2_combo"}, + {"type1_cable", "type1"}, + }; + + auto itMap = kTypeMap.find(type); + if (itMap != kTypeMap.end()) + type = itMap->second; + + // only store sockets type that are relevant to EV charging + static std::unordered_set const SUPPORTED_TYPES = { + "type1", "type1_combo", "type2", "type2_cable", "type2_combo", "chademo", "nacs", + "gb_ac", "gb_dc", "chaoji", "type3a", "type3c", "mcs"}; + + if (SUPPORTED_TYPES.find(type) == SUPPORTED_TYPES.end()) + return; // unknown type -> ignore + + // find or create descriptor + auto it = std::find_if(m_chargeSockets.begin(), m_chargeSockets.end(), + [&](ChargeSocketDescriptor const & d) { return d.type == type; }); + + if (it == m_chargeSockets.end()) + { + m_chargeSockets.push_back({type, "y", ""}); + it = std::prev(m_chargeSockets.end()); + } + + ASSERT(v.size() > 0, "empty value for socket key!"); + + if (!isOutput) + { + if (v == "yes") + { + it->count = "y"; + } + else + { + // try to parse count as a number + try + { + auto count = std::stoi(v); + if (count <= 0) + { + LOG(LWARNING, ("Invalid socket count. Removing this socket.", "")); + m_chargeSockets.pop_back(); + return; + } + } + catch (...) + { + // ignore sockets with invalid counts (ie, can not be parsed to int) + // note that if a valid power output is later set for this socket, + // the socket will be re-created with a default count of 'y' + LOG(LWARNING, ("Invalid count of charging socket. Removing it.", v)); + m_chargeSockets.pop_back(); + return; + } + it->count = v; + } + } + else // isOutput == true => parse output power + { + // example value string: "44;22kW;11kva;7400w" + + std::string powerValues = strings::MakeLowerCase(v); + + // replace all occurances of 'VA' by the more standard 'W' unit + size_t pos = powerValues.find("va"); + while (pos != powerValues.npos) + { + powerValues.replace(pos, 2, "w"); + pos = powerValues.find("va", pos + 1); + } + + // if a given socket type is present several times in the same charging + // station with different power outputs, the power outputs would be concatenated + // with ';' + auto powerTokens = strings::Tokenize(powerValues, ";/"); + + // TODO: for now, we only handle the *first* provided + // power output. + std::string num(powerTokens[0]); + strings::Trim(num); + + if (num == "unknown") + { + it->output_kW = ""; + return; + } + + enum PowerUnit + { + WATT, + KILOWATT, + MEGAWATT + }; + PowerUnit unit = KILOWATT; // if no unit, kW are assumed + + if (num.size() > 2) + { + // do we have a unit? + if (num.back() == 'w') + { + unit = WATT; + num.pop_back(); + if (num.back() == 'k') + { + unit = KILOWATT; + num.pop_back(); + } + else if (num.back() == 'm') + { + unit = MEGAWATT; + num.pop_back(); + } + } + } + + strings::Trim(num); + try + { + double value = std::stod(num); + std::ostringstream oss; + switch (unit) + { + case WATT: oss << value / 1000.; break; + case MEGAWATT: oss << value * 1000; break; + case KILOWATT: oss << value; break; + } + num = oss.str(); + } + catch (...) + { + LOG(LWARNING, ("Invalid charging socket power value:", v)); + num = ""; + } + + it->output_kW = num; + } +} + +std::string MetadataTagProcessorImpl::StringifyChargeSockets() const +{ + std::ostringstream oss; + + for (size_t i = 0; i < m_chargeSockets.size(); ++i) + { + auto const & desc = m_chargeSockets[i]; + + oss << desc.type << "|" << desc.count << "|" << desc.output_kW; + + if (i + 1 < m_chargeSockets.size()) + oss << ";"; + } + return oss.str(); +} + std::string MetadataTagProcessorImpl::ValidateAndFormat_stars(std::string const & v) { if (v.empty()) @@ -523,6 +708,12 @@ MetadataTagProcessor::~MetadataTagProcessor() { if (!m_description.IsEmpty()) m_params.GetMetadata().Set(feature::Metadata::FMD_DESCRIPTION, m_description.GetBuffer()); + + if (!m_chargeSockets.empty()) + { + auto socketsList = StringifyChargeSockets(); + m_params.GetMetadata().Set(feature::Metadata::FMD_CHARGE_SOCKETS, socketsList); + } } void MetadataTagProcessor::operator()(std::string const & k, std::string const & v) @@ -630,6 +821,8 @@ void MetadataTagProcessor::operator()(std::string const & k, std::string const & case Metadata::FMD_SELF_SERVICE: valid = ValidateAndFormat_self_service(v); break; case Metadata::FMD_OUTDOOR_SEATING: valid = ValidateAndFormat_outdoor_seating(v); break; case Metadata::FMD_NETWORK: valid = ValidateAndFormat_operator(v); break; + case Metadata::FMD_CHARGE_SOCKETS: AggregateChargeSocket(k, v); break; + // Metadata types we do not get from OSM. case Metadata::FMD_CUISINE: case Metadata::FMD_DESCRIPTION: // processed separately diff --git a/generator/osm2meta.hpp b/generator/osm2meta.hpp index f543c5cde..028cbb2d9 100644 --- a/generator/osm2meta.hpp +++ b/generator/osm2meta.hpp @@ -9,6 +9,23 @@ struct MetadataTagProcessorImpl { MetadataTagProcessorImpl(FeatureBuilderParams & params) : m_params(params) {} + /** Parse OSM attributes for socket types and add them to m_chargeSockets. + * + * Examples of (k,v) pairs: + * ("socket:type2_combo", "2") + * ("socket:type2_combo:output", "150 kW") + * ("socket:chademo", "1") + * ("socket:chademo:output", "50") // assumes kW + */ + void AggregateChargeSocket(std::string const & k, std::string const & v); + + /** Output the list of all sockets for a given charging station in the format + * ||[];... + * + * For instance: + * "type2_combo|2|150;chademo|1|50;type2|2|" + */ + std::string StringifyChargeSockets() const; std::string ValidateAndFormat_maxspeed(std::string const & v) const; static std::string ValidateAndFormat_stars(std::string const & v); std::string ValidateAndFormat_operator(std::string const & v) const; @@ -45,6 +62,21 @@ struct MetadataTagProcessorImpl static std::string ValidateAndFormat_outdoor_seating(std::string v); protected: + // struct to store the representation of a charging station socket + struct ChargeSocketDescriptor + { + std::string type; // https://wiki.openstreetmap.org/wiki/Key:socket:* + // e.g. "type1" + std::string count; // number of sockets or 'y' if OSM tag was set to 'yes'. + // ("" if unknown) + std::string output_kW; // optional power output, in kW ("" if unknown) + }; + typedef std::vector ChargeSocketDescriptors; + + // stores information about charge sockets in charging stations. + // Incrementally completed in AggregateChargeSocket + ChargeSocketDescriptors m_chargeSockets; + FeatureBuilderParams & m_params; }; diff --git a/libs/indexer/feature_meta.cpp b/libs/indexer/feature_meta.cpp index 31a3b35ff..2e58c23f7 100644 --- a/libs/indexer/feature_meta.cpp +++ b/libs/indexer/feature_meta.cpp @@ -194,6 +194,8 @@ bool Metadata::TypeFromString(string_view k, Metadata::EType & outType) outType = Metadata::FMD_OUTDOOR_SEATING; else if (k == "network") outType = Metadata::FMD_NETWORK; + else if (k.starts_with("socket:")) + outType = Metadata::FMD_CHARGE_SOCKETS; else return false; @@ -315,6 +317,7 @@ string ToString(Metadata::EType type) case Metadata::FMD_SELF_SERVICE: return "self_service"; case Metadata::FMD_OUTDOOR_SEATING: return "outdoor_seating"; case Metadata::FMD_NETWORK: return "network"; + case Metadata::FMD_CHARGE_SOCKETS: CHECK(false, ("FMD_CHARGE_SOCKETS is a compound attribute.")); case Metadata::FMD_COUNT: CHECK(false, ("FMD_COUNT can not be used as a type.")); }; diff --git a/libs/indexer/feature_meta.hpp b/libs/indexer/feature_meta.hpp index 652e7e947..6e06fa58d 100644 --- a/libs/indexer/feature_meta.hpp +++ b/libs/indexer/feature_meta.hpp @@ -124,6 +124,7 @@ public: FMD_CHECK_DATE = 53, FMD_CHECK_DATE_OPEN_HOURS = 54, FMD_BRANCH = 55, + FMD_CHARGE_SOCKETS = 56, FMD_COUNT }; diff --git a/libs/indexer/map_object.cpp b/libs/indexer/map_object.cpp index b3315a563..564d3ddc3 100644 --- a/libs/indexer/map_object.cpp +++ b/libs/indexer/map_object.cpp @@ -178,6 +178,57 @@ std::string_view MapObject::GetOpeningHours() const return m_metadata.Get(MetadataID::FMD_OPEN_HOURS); } +ChargeSocketDescriptors MapObject::GetChargeSockets() const +{ + ChargeSocketDescriptors sockets; + + auto s = std::string(m_metadata.Get(MetadataID::FMD_CHARGE_SOCKETS)); + if (s.empty()) + return sockets; + + auto tokens = strings::Tokenize(s, ";"); + + for (auto token : tokens) + { + if (token.empty()) + continue; + + auto fields = strings::Tokenize(token, "|"); + + if (fields.size() < 3) + continue; // invalid entry, skip + + ChargeSocketDescriptor desc; + desc.type = fields[0]; + + try + { + desc.count = std::stoi(std::string(fields[1])); + } + catch (...) + { + desc.count = 0; + } + + if (fields.size() >= 3) + { + try + { + desc.power = std::stod(std::string(fields[2])); + } + catch (...) + { + desc.power = 0; + } + } + else + desc.power = 0; + + sockets.push_back(desc); + } + return sockets; +} + feature::Internet MapObject::GetInternet() const { return feature::InternetFromString(m_metadata.Get(MetadataID::FMD_INTERNET)); @@ -242,6 +293,11 @@ int MapObject::GetStars() const return count; } +std::string MapObject::GetCapacity() const +{ + return std::string(m_metadata.Get(MetadataID::FMD_CAPACITY)); +} + bool MapObject::IsPointType() const { return m_geomType == feature::GeomType::Point; diff --git a/libs/indexer/map_object.hpp b/libs/indexer/map_object.hpp index aa34e429f..1b612fe20 100644 --- a/libs/indexer/map_object.hpp +++ b/libs/indexer/map_object.hpp @@ -17,6 +17,17 @@ namespace osm { class EditableMapObject; +// struct to store the representation of a charging station socket +struct ChargeSocketDescriptor +{ + std::string type; // https://wiki.openstreetmap.org/wiki/Key:socket:* + // e.g. "type1" + unsigned int count; // number of sockets; 0 means socket present, but unknown count + // (eg, OSM tag for count set to 'yes') + double power; // power output, in kW. 0 means unknown. +}; +typedef std::vector ChargeSocketDescriptors; + class MapObject { public: @@ -80,9 +91,19 @@ public: std::string FormatRoadShields() const; + /** parses a list of charging station sockets + * stored as "||[];..." into a vector of + * socket descriptors + * + * For instance: + * "type2_combo|2|150;chademo|1|50;type2|4|" + */ + ChargeSocketDescriptors GetChargeSockets() const; + std::string_view GetOpeningHours() const; feature::Internet GetInternet() const; int GetStars() const; + std::string GetCapacity() const; /// @returns true if feature has ATM type. bool HasAtm() const; diff --git a/qt/place_page_dialog_user.cpp b/qt/place_page_dialog_user.cpp index 3e6d12f0e..0309db809 100644 --- a/qt/place_page_dialog_user.cpp +++ b/qt/place_page_dialog_user.cpp @@ -161,6 +161,26 @@ PlacePageDialogUser::PlacePageDialogUser(QWidget * parent, place_page::Info cons if (auto cuisines = info.FormatCuisines(); !cuisines.empty()) addEntry("Cuisine", cuisines); + // Capacity fragment + if (auto capacity = info.GetCapacity(); !capacity.empty()) + addEntry("Capacity", capacity); + + // Sockets fragment + if (auto sockets = info.GetChargeSockets(); !sockets.empty()) + { + std::ostringstream oss; + for (auto s : sockets) + { + oss << s.type; + if (s.power > 0) + oss << " (" << s.power << "kW)"; + if (s.count > 0) + oss << " × " << s.count; + oss << "\n"; + } + addEntry("Charging sockets", oss.str()); + } + // Entrance fragment // TODO