mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-19 04:53:36 +00:00
[generator] retrieve socket:* OSM tags used by amenity:charging_station
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 <severin@guakamole.org>
This commit is contained in:
committed by
skadge
parent
de6953598b
commit
f8d786958a
@@ -70,7 +70,9 @@ public class Metadata implements Parcelable
|
|||||||
FMD_CONTACT_BLUESKY(51),
|
FMD_CONTACT_BLUESKY(51),
|
||||||
FMD_PANORAMAX(52),
|
FMD_PANORAMAX(52),
|
||||||
FMD_CHECK_DATE(53),
|
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;
|
private final int mMetaType;
|
||||||
|
|
||||||
MetadataType(int metadataType)
|
MetadataType(int metadataType)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -81,9 +82,193 @@ bool Prefix2Double(std::string const & str, double & d)
|
|||||||
d = std::strtod(s, &stop);
|
d = std::strtod(s, &stop);
|
||||||
return (s != stop && math::is_finite(d));
|
return (s != stop && math::is_finite(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // 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<std::string, std::string> 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<std::string> 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)
|
std::string MetadataTagProcessorImpl::ValidateAndFormat_stars(std::string const & v)
|
||||||
{
|
{
|
||||||
if (v.empty())
|
if (v.empty())
|
||||||
@@ -523,6 +708,12 @@ MetadataTagProcessor::~MetadataTagProcessor()
|
|||||||
{
|
{
|
||||||
if (!m_description.IsEmpty())
|
if (!m_description.IsEmpty())
|
||||||
m_params.GetMetadata().Set(feature::Metadata::FMD_DESCRIPTION, m_description.GetBuffer());
|
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)
|
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_SELF_SERVICE: valid = ValidateAndFormat_self_service(v); break;
|
||||||
case Metadata::FMD_OUTDOOR_SEATING: valid = ValidateAndFormat_outdoor_seating(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_NETWORK: valid = ValidateAndFormat_operator(v); break;
|
||||||
|
case Metadata::FMD_CHARGE_SOCKETS: AggregateChargeSocket(k, v); break;
|
||||||
|
|
||||||
// Metadata types we do not get from OSM.
|
// Metadata types we do not get from OSM.
|
||||||
case Metadata::FMD_CUISINE:
|
case Metadata::FMD_CUISINE:
|
||||||
case Metadata::FMD_DESCRIPTION: // processed separately
|
case Metadata::FMD_DESCRIPTION: // processed separately
|
||||||
|
|||||||
@@ -9,6 +9,23 @@ struct MetadataTagProcessorImpl
|
|||||||
{
|
{
|
||||||
MetadataTagProcessorImpl(FeatureBuilderParams & params) : m_params(params) {}
|
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
|
||||||
|
* <type>|<nb>|[<power>];...
|
||||||
|
*
|
||||||
|
* For instance:
|
||||||
|
* "type2_combo|2|150;chademo|1|50;type2|2|"
|
||||||
|
*/
|
||||||
|
std::string StringifyChargeSockets() const;
|
||||||
std::string ValidateAndFormat_maxspeed(std::string const & v) const;
|
std::string ValidateAndFormat_maxspeed(std::string const & v) const;
|
||||||
static std::string ValidateAndFormat_stars(std::string const & v);
|
static std::string ValidateAndFormat_stars(std::string const & v);
|
||||||
std::string ValidateAndFormat_operator(std::string const & v) const;
|
std::string ValidateAndFormat_operator(std::string const & v) const;
|
||||||
@@ -45,6 +62,21 @@ struct MetadataTagProcessorImpl
|
|||||||
static std::string ValidateAndFormat_outdoor_seating(std::string v);
|
static std::string ValidateAndFormat_outdoor_seating(std::string v);
|
||||||
|
|
||||||
protected:
|
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<ChargeSocketDescriptor> ChargeSocketDescriptors;
|
||||||
|
|
||||||
|
// stores information about charge sockets in charging stations.
|
||||||
|
// Incrementally completed in AggregateChargeSocket
|
||||||
|
ChargeSocketDescriptors m_chargeSockets;
|
||||||
|
|
||||||
FeatureBuilderParams & m_params;
|
FeatureBuilderParams & m_params;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ bool Metadata::TypeFromString(string_view k, Metadata::EType & outType)
|
|||||||
outType = Metadata::FMD_OUTDOOR_SEATING;
|
outType = Metadata::FMD_OUTDOOR_SEATING;
|
||||||
else if (k == "network")
|
else if (k == "network")
|
||||||
outType = Metadata::FMD_NETWORK;
|
outType = Metadata::FMD_NETWORK;
|
||||||
|
else if (k.starts_with("socket:"))
|
||||||
|
outType = Metadata::FMD_CHARGE_SOCKETS;
|
||||||
else
|
else
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -315,6 +317,7 @@ string ToString(Metadata::EType type)
|
|||||||
case Metadata::FMD_SELF_SERVICE: return "self_service";
|
case Metadata::FMD_SELF_SERVICE: return "self_service";
|
||||||
case Metadata::FMD_OUTDOOR_SEATING: return "outdoor_seating";
|
case Metadata::FMD_OUTDOOR_SEATING: return "outdoor_seating";
|
||||||
case Metadata::FMD_NETWORK: return "network";
|
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."));
|
case Metadata::FMD_COUNT: CHECK(false, ("FMD_COUNT can not be used as a type."));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ public:
|
|||||||
FMD_CHECK_DATE = 53,
|
FMD_CHECK_DATE = 53,
|
||||||
FMD_CHECK_DATE_OPEN_HOURS = 54,
|
FMD_CHECK_DATE_OPEN_HOURS = 54,
|
||||||
FMD_BRANCH = 55,
|
FMD_BRANCH = 55,
|
||||||
|
FMD_CHARGE_SOCKETS = 56,
|
||||||
FMD_COUNT
|
FMD_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,57 @@ std::string_view MapObject::GetOpeningHours() const
|
|||||||
return m_metadata.Get(MetadataID::FMD_OPEN_HOURS);
|
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
|
feature::Internet MapObject::GetInternet() const
|
||||||
{
|
{
|
||||||
return feature::InternetFromString(m_metadata.Get(MetadataID::FMD_INTERNET));
|
return feature::InternetFromString(m_metadata.Get(MetadataID::FMD_INTERNET));
|
||||||
@@ -242,6 +293,11 @@ int MapObject::GetStars() const
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string MapObject::GetCapacity() const
|
||||||
|
{
|
||||||
|
return std::string(m_metadata.Get(MetadataID::FMD_CAPACITY));
|
||||||
|
}
|
||||||
|
|
||||||
bool MapObject::IsPointType() const
|
bool MapObject::IsPointType() const
|
||||||
{
|
{
|
||||||
return m_geomType == feature::GeomType::Point;
|
return m_geomType == feature::GeomType::Point;
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ namespace osm
|
|||||||
{
|
{
|
||||||
class EditableMapObject;
|
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<ChargeSocketDescriptor> ChargeSocketDescriptors;
|
||||||
|
|
||||||
class MapObject
|
class MapObject
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -80,9 +91,19 @@ public:
|
|||||||
|
|
||||||
std::string FormatRoadShields() const;
|
std::string FormatRoadShields() const;
|
||||||
|
|
||||||
|
/** parses a list of charging station sockets
|
||||||
|
* stored as "<type>|<nb>|[<power>];..." 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;
|
std::string_view GetOpeningHours() const;
|
||||||
feature::Internet GetInternet() const;
|
feature::Internet GetInternet() const;
|
||||||
int GetStars() const;
|
int GetStars() const;
|
||||||
|
std::string GetCapacity() const;
|
||||||
|
|
||||||
/// @returns true if feature has ATM type.
|
/// @returns true if feature has ATM type.
|
||||||
bool HasAtm() const;
|
bool HasAtm() const;
|
||||||
|
|||||||
@@ -161,6 +161,26 @@ PlacePageDialogUser::PlacePageDialogUser(QWidget * parent, place_page::Info cons
|
|||||||
if (auto cuisines = info.FormatCuisines(); !cuisines.empty())
|
if (auto cuisines = info.FormatCuisines(); !cuisines.empty())
|
||||||
addEntry("Cuisine", cuisines);
|
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
|
// Entrance fragment
|
||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user