mirror of
https://github.com/tumic0/GPXSee.git
synced 2025-06-27 11:39:16 +02:00
Added cadence and power graphs
This commit is contained in:
84
src/cadencegraph.cpp
Normal file
84
src/cadencegraph.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
#include "data.h"
|
||||
#include "cadencegraph.h"
|
||||
|
||||
|
||||
CadenceGraph::CadenceGraph(QWidget *parent) : GraphTab(parent)
|
||||
{
|
||||
_units = Metric;
|
||||
_showTracks = true;
|
||||
|
||||
GraphView::setYUnits(tr("1/min"));
|
||||
setYLabel(tr("Cadence"));
|
||||
|
||||
setSliderPrecision(1);
|
||||
}
|
||||
|
||||
void CadenceGraph::setInfo()
|
||||
{
|
||||
if (_showTracks) {
|
||||
GraphView::addInfo(tr("Average"), QString::number(avg() * yScale()
|
||||
+ yOffset(), 'f', 1) + UNIT_SPACE + yUnits());
|
||||
GraphView::addInfo(tr("Maximum"), QString::number(max() * yScale()
|
||||
+ yOffset(), 'f', 1) + UNIT_SPACE + yUnits());
|
||||
} else
|
||||
clearInfo();
|
||||
}
|
||||
|
||||
void CadenceGraph::loadData(const Data &data, const QList<PathItem *> &paths)
|
||||
{
|
||||
for (int i = 0; i < data.tracks().count(); i++) {
|
||||
const Graph &graph = data.tracks().at(i)->cadence();
|
||||
qreal sum = 0, w = 0;
|
||||
|
||||
if (graph.size() < 2) {
|
||||
skipColor();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 1; j < graph.size(); j++) {
|
||||
qreal ds = graph.at(j).s() - graph.at(j-1).s();
|
||||
sum += graph.at(j).y() * ds;
|
||||
w += ds;
|
||||
}
|
||||
_avg.append(QPointF(data.tracks().at(i)->distance(), sum/w));
|
||||
|
||||
GraphView::loadGraph(graph, paths.at(i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.routes().count(); i++)
|
||||
skipColor();
|
||||
|
||||
setInfo();
|
||||
|
||||
redraw();
|
||||
}
|
||||
|
||||
qreal CadenceGraph::avg() const
|
||||
{
|
||||
qreal sum = 0, w = 0;
|
||||
QList<QPointF>::const_iterator it;
|
||||
|
||||
for (it = _avg.begin(); it != _avg.end(); it++) {
|
||||
sum += it->y() * it->x();
|
||||
w += it->x();
|
||||
}
|
||||
|
||||
return (sum / w);
|
||||
}
|
||||
|
||||
void CadenceGraph::clear()
|
||||
{
|
||||
_avg.clear();
|
||||
|
||||
GraphView::clear();
|
||||
}
|
||||
|
||||
void CadenceGraph::showTracks(bool show)
|
||||
{
|
||||
_showTracks = show;
|
||||
|
||||
showGraph(show);
|
||||
setInfo();
|
||||
|
||||
redraw();
|
||||
}
|
31
src/cadencegraph.h
Normal file
31
src/cadencegraph.h
Normal file
@ -0,0 +1,31 @@
|
||||
#ifndef CADENCEGRAPH_H
|
||||
#define CADENCEGRAPH_H
|
||||
|
||||
#include "graphtab.h"
|
||||
|
||||
class CadenceGraph : public GraphTab
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
CadenceGraph(QWidget *parent = 0);
|
||||
|
||||
QString label() const {return tr("Cadence");}
|
||||
void loadData(const Data &data, const QList<PathItem *> &paths);
|
||||
void clear();
|
||||
void setUnits(enum Units) {}
|
||||
void showTracks(bool show);
|
||||
void showRoutes(bool show) {Q_UNUSED(show);}
|
||||
|
||||
private:
|
||||
qreal avg() const;
|
||||
qreal max() const {return bounds().bottom();}
|
||||
void setInfo();
|
||||
|
||||
QList<QPointF> _avg;
|
||||
|
||||
enum Units _units;
|
||||
bool _showTracks;
|
||||
};
|
||||
|
||||
#endif // CADENCEGRAPH_H
|
@ -230,10 +230,18 @@ bool FITParser::parseData(MessageDefinition *def, quint8 offset)
|
||||
if (val != 0xff)
|
||||
trackpoint.setHeartRate(val);
|
||||
break;
|
||||
case 4:
|
||||
if (val != 0xff)
|
||||
trackpoint.setCadence(val);
|
||||
break;
|
||||
case 6:
|
||||
if (val != 0xffff)
|
||||
trackpoint.setSpeed(val / 1000.0f);
|
||||
break;
|
||||
case 7:
|
||||
if (val != 0xffff)
|
||||
trackpoint.setPower(val);
|
||||
break;
|
||||
case 13:
|
||||
if (val != 0x7f)
|
||||
trackpoint.setTemperature((qint8)val);
|
||||
|
@ -51,56 +51,13 @@ Coordinates GPXParser::coordinates()
|
||||
return Coordinates(lon, lat);
|
||||
}
|
||||
|
||||
void GPXParser::handleTrackpointData(DataType type, Trackpoint &trackpoint)
|
||||
{
|
||||
switch (type) {
|
||||
case Elevation:
|
||||
trackpoint.setElevation(number());
|
||||
break;
|
||||
case Time:
|
||||
trackpoint.setTimestamp(time());
|
||||
break;
|
||||
case Speed:
|
||||
trackpoint.setSpeed(number());
|
||||
break;
|
||||
case HeartRate:
|
||||
trackpoint.setHeartRate(number());
|
||||
break;
|
||||
case Temperature:
|
||||
trackpoint.setTemperature(number());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GPXParser::handleWaypointData(DataType type, Waypoint &waypoint)
|
||||
{
|
||||
switch (type) {
|
||||
case Name:
|
||||
waypoint.setName(_reader.readElementText());
|
||||
break;
|
||||
case Description:
|
||||
waypoint.setDescription(_reader.readElementText());
|
||||
break;
|
||||
case Time:
|
||||
waypoint.setTimestamp(time());
|
||||
break;
|
||||
case Elevation:
|
||||
waypoint.setElevation(number());
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GPXParser::tpExtension(Trackpoint &trackpoint)
|
||||
{
|
||||
while (_reader.readNextStartElement()) {
|
||||
if (_reader.name() == "hr")
|
||||
handleTrackpointData(HeartRate, trackpoint);
|
||||
trackpoint.setHeartRate(number());
|
||||
else if (_reader.name() == "atemp")
|
||||
handleTrackpointData(Temperature, trackpoint);
|
||||
trackpoint.setTemperature(number());
|
||||
else
|
||||
_reader.skipCurrentElement();
|
||||
}
|
||||
@ -110,11 +67,15 @@ void GPXParser::extensions(Trackpoint &trackpoint)
|
||||
{
|
||||
while (_reader.readNextStartElement()) {
|
||||
if (_reader.name() == "speed")
|
||||
handleTrackpointData(Speed, trackpoint);
|
||||
trackpoint.setSpeed(number());
|
||||
else if (_reader.name() == "hr" || _reader.name() == "heartrate")
|
||||
handleTrackpointData(HeartRate, trackpoint);
|
||||
trackpoint.setHeartRate(number());
|
||||
else if (_reader.name() == "temp")
|
||||
handleTrackpointData(Temperature, trackpoint);
|
||||
trackpoint.setTemperature(number());
|
||||
else if (_reader.name() == "cadence")
|
||||
trackpoint.setCadence(number());
|
||||
else if (_reader.name() == "power")
|
||||
trackpoint.setPower(number());
|
||||
else if (_reader.name() == "TrackPointExtension")
|
||||
tpExtension(trackpoint);
|
||||
else
|
||||
@ -128,9 +89,9 @@ void GPXParser::trackpointData(Trackpoint &trackpoint)
|
||||
|
||||
while (_reader.readNextStartElement()) {
|
||||
if (_reader.name() == "ele")
|
||||
handleTrackpointData(Elevation, trackpoint);
|
||||
trackpoint.setElevation(number());
|
||||
else if (_reader.name() == "time")
|
||||
handleTrackpointData(Time, trackpoint);
|
||||
trackpoint.setTimestamp(time());
|
||||
else if (_reader.name() == "geoidheight")
|
||||
gh = number();
|
||||
else if (_reader.name() == "extensions")
|
||||
@ -149,15 +110,15 @@ void GPXParser::waypointData(Waypoint &waypoint)
|
||||
|
||||
while (_reader.readNextStartElement()) {
|
||||
if (_reader.name() == "name")
|
||||
handleWaypointData(Name, waypoint);
|
||||
waypoint.setName(_reader.readElementText());
|
||||
else if (_reader.name() == "desc")
|
||||
handleWaypointData(Description, waypoint);
|
||||
waypoint.setDescription(_reader.readElementText());
|
||||
else if (_reader.name() == "ele")
|
||||
handleWaypointData(Elevation, waypoint);
|
||||
waypoint.setElevation(number());
|
||||
else if (_reader.name() == "geoidheight")
|
||||
gh = number();
|
||||
else if (_reader.name() == "time")
|
||||
handleWaypointData(Time, waypoint);
|
||||
waypoint.setTimestamp(time());
|
||||
else
|
||||
_reader.skipCurrentElement();
|
||||
}
|
||||
|
@ -17,10 +17,6 @@ public:
|
||||
int errorLine() const {return _reader.lineNumber();}
|
||||
|
||||
private:
|
||||
enum DataType {
|
||||
Name, Description, Elevation, Time, Speed, HeartRate, Temperature
|
||||
};
|
||||
|
||||
bool parse();
|
||||
void gpx();
|
||||
void track(TrackData &track);
|
||||
@ -34,9 +30,6 @@ private:
|
||||
QDateTime time();
|
||||
Coordinates coordinates();
|
||||
|
||||
void handleWaypointData(DataType type, Waypoint &waypoint);
|
||||
void handleTrackpointData(DataType type, Trackpoint &trackpoint);
|
||||
|
||||
QXmlStreamReader _reader;
|
||||
};
|
||||
|
||||
|
@ -33,6 +33,8 @@
|
||||
#include "speedgraph.h"
|
||||
#include "heartrategraph.h"
|
||||
#include "temperaturegraph.h"
|
||||
#include "cadencegraph.h"
|
||||
#include "powergraph.h"
|
||||
#include "pathview.h"
|
||||
#include "trackinfo.h"
|
||||
#include "filebrowser.h"
|
||||
@ -512,6 +514,8 @@ void GUI::createGraphTabs()
|
||||
_tabs.append(new ElevationGraph);
|
||||
_tabs.append(new SpeedGraph);
|
||||
_tabs.append(new HeartRateGraph);
|
||||
_tabs.append(new CadenceGraph);
|
||||
_tabs.append(new PowerGraph);
|
||||
_tabs.append(new TemperatureGraph);
|
||||
|
||||
for (int i = 0; i < _tabs.count(); i++)
|
||||
|
84
src/powergraph.cpp
Normal file
84
src/powergraph.cpp
Normal file
@ -0,0 +1,84 @@
|
||||
#include "data.h"
|
||||
#include "powergraph.h"
|
||||
|
||||
|
||||
PowerGraph::PowerGraph(QWidget *parent) : GraphTab(parent)
|
||||
{
|
||||
_units = Metric;
|
||||
_showTracks = true;
|
||||
|
||||
GraphView::setYUnits(tr("W"));
|
||||
setYLabel(tr("Power"));
|
||||
|
||||
setSliderPrecision(1);
|
||||
}
|
||||
|
||||
void PowerGraph::setInfo()
|
||||
{
|
||||
if (_showTracks) {
|
||||
GraphView::addInfo(tr("Average"), QString::number(avg() * yScale()
|
||||
+ yOffset(), 'f', 1) + UNIT_SPACE + yUnits());
|
||||
GraphView::addInfo(tr("Maximum"), QString::number(max() * yScale()
|
||||
+ yOffset(), 'f', 1) + UNIT_SPACE + yUnits());
|
||||
} else
|
||||
clearInfo();
|
||||
}
|
||||
|
||||
void PowerGraph::loadData(const Data &data, const QList<PathItem *> &paths)
|
||||
{
|
||||
for (int i = 0; i < data.tracks().count(); i++) {
|
||||
const Graph &graph = data.tracks().at(i)->power();
|
||||
qreal sum = 0, w = 0;
|
||||
|
||||
if (graph.size() < 2) {
|
||||
skipColor();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 1; j < graph.size(); j++) {
|
||||
qreal ds = graph.at(j).s() - graph.at(j-1).s();
|
||||
sum += graph.at(j).y() * ds;
|
||||
w += ds;
|
||||
}
|
||||
_avg.append(QPointF(data.tracks().at(i)->distance(), sum/w));
|
||||
|
||||
GraphView::loadGraph(graph, paths.at(i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < data.routes().count(); i++)
|
||||
skipColor();
|
||||
|
||||
setInfo();
|
||||
|
||||
redraw();
|
||||
}
|
||||
|
||||
qreal PowerGraph::avg() const
|
||||
{
|
||||
qreal sum = 0, w = 0;
|
||||
QList<QPointF>::const_iterator it;
|
||||
|
||||
for (it = _avg.begin(); it != _avg.end(); it++) {
|
||||
sum += it->y() * it->x();
|
||||
w += it->x();
|
||||
}
|
||||
|
||||
return (sum / w);
|
||||
}
|
||||
|
||||
void PowerGraph::clear()
|
||||
{
|
||||
_avg.clear();
|
||||
|
||||
GraphView::clear();
|
||||
}
|
||||
|
||||
void PowerGraph::showTracks(bool show)
|
||||
{
|
||||
_showTracks = show;
|
||||
|
||||
showGraph(show);
|
||||
setInfo();
|
||||
|
||||
redraw();
|
||||
}
|
31
src/powergraph.h
Normal file
31
src/powergraph.h
Normal file
@ -0,0 +1,31 @@
|
||||
#ifndef POWERGRAPH_H
|
||||
#define POWERGRAPH_H
|
||||
|
||||
#include "graphtab.h"
|
||||
|
||||
class PowerGraph : public GraphTab
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PowerGraph(QWidget *parent = 0);
|
||||
|
||||
QString label() const {return tr("Power");}
|
||||
void loadData(const Data &data, const QList<PathItem *> &paths);
|
||||
void clear();
|
||||
void setUnits(enum Units) {}
|
||||
void showTracks(bool show);
|
||||
void showRoutes(bool show) {Q_UNUSED(show);}
|
||||
|
||||
private:
|
||||
qreal avg() const;
|
||||
qreal max() const {return bounds().bottom();}
|
||||
void setInfo();
|
||||
|
||||
QList<QPointF> _avg;
|
||||
|
||||
enum Units _units;
|
||||
bool _showTracks;
|
||||
};
|
||||
|
||||
#endif // POWERGRAPH_H
|
@ -56,6 +56,18 @@ Coordinates TCXParser::position()
|
||||
return pos;
|
||||
}
|
||||
|
||||
void TCXParser::extensions(Trackpoint &trackpoint)
|
||||
{
|
||||
while (_reader.readNextStartElement()) {
|
||||
if (_reader.name() == "RunCadence")
|
||||
trackpoint.setCadence(number());
|
||||
else if (_reader.name() == "Watts")
|
||||
trackpoint.setPower(number());
|
||||
else
|
||||
_reader.skipCurrentElement();
|
||||
}
|
||||
}
|
||||
|
||||
void TCXParser::trackpointData(Trackpoint &trackpoint)
|
||||
{
|
||||
while (_reader.readNextStartElement()) {
|
||||
@ -67,6 +79,10 @@ void TCXParser::trackpointData(Trackpoint &trackpoint)
|
||||
trackpoint.setTimestamp(time());
|
||||
else if (_reader.name() == "HeartRateBpm")
|
||||
trackpoint.setHeartRate(number());
|
||||
else if (_reader.name() == "Cadence")
|
||||
trackpoint.setCadence(number());
|
||||
else if (_reader.name() == "Extensions")
|
||||
extensions(trackpoint);
|
||||
else
|
||||
_reader.skipCurrentElement();
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ private:
|
||||
void trackpoints(TrackData &track);
|
||||
void trackpointData(Trackpoint &trackpoint);
|
||||
void waypointData(Waypoint &waypoint);
|
||||
void extensions(Trackpoint &trackpoint);
|
||||
Coordinates position();
|
||||
qreal number();
|
||||
QDateTime time();
|
||||
|
@ -6,6 +6,8 @@
|
||||
#define WINDOW_EF 3
|
||||
#define WINDOW_SF 7
|
||||
#define WINDOW_HF 3
|
||||
#define WINDOW_CF 3
|
||||
#define WINDOW_PF 3
|
||||
|
||||
|
||||
static qreal median(QVector<qreal> v)
|
||||
@ -114,14 +116,10 @@ Graph Track::elevation() const
|
||||
if (!_data.size())
|
||||
return raw;
|
||||
|
||||
for (int i = 0; i < _data.size(); i++) {
|
||||
if (_outliers.contains(i))
|
||||
continue;
|
||||
|
||||
if (_data.at(i).hasElevation())
|
||||
for (int i = 0; i < _data.size(); i++)
|
||||
if (_data.at(i).hasElevation() && !_outliers.contains(i))
|
||||
raw.append(GraphPoint(_distance.at(i), _time.at(i),
|
||||
_data.at(i).elevation()));
|
||||
}
|
||||
|
||||
return filter(raw, WINDOW_EF);
|
||||
}
|
||||
@ -135,12 +133,9 @@ Graph Track::speed() const
|
||||
return raw;
|
||||
|
||||
for (int i = 0; i < _data.size(); i++) {
|
||||
if (_outliers.contains(i))
|
||||
continue;
|
||||
|
||||
if (_data.at(i).hasSpeed())
|
||||
if (_data.at(i).hasSpeed() && !_outliers.contains(i))
|
||||
v = _data.at(i).speed();
|
||||
else if (!std::isnan(_speed.at(i)))
|
||||
else if (!std::isnan(_speed.at(i)) && !_outliers.contains(i))
|
||||
v = _speed.at(i);
|
||||
else
|
||||
continue;
|
||||
@ -158,14 +153,10 @@ Graph Track::heartRate() const
|
||||
if (!_data.size())
|
||||
return raw;
|
||||
|
||||
for (int i = 0; i < _data.count(); i++) {
|
||||
if (_outliers.contains(i))
|
||||
continue;
|
||||
|
||||
if (_data.at(i).hasHeartRate())
|
||||
for (int i = 0; i < _data.count(); i++)
|
||||
if (_data.at(i).hasHeartRate() && !_outliers.contains(i))
|
||||
raw.append(GraphPoint(_distance.at(i), _time.at(i),
|
||||
_data.at(i).heartRate()));
|
||||
}
|
||||
|
||||
return filter(raw, WINDOW_HF);
|
||||
}
|
||||
@ -174,16 +165,36 @@ Graph Track::temperature() const
|
||||
{
|
||||
Graph raw;
|
||||
|
||||
for (int i = 0; i < _data.size(); i++) {
|
||||
if (_outliers.contains(i))
|
||||
continue;
|
||||
|
||||
if (_data.at(i).hasTemperature())
|
||||
for (int i = 0; i < _data.size(); i++)
|
||||
if (_data.at(i).hasTemperature() && !_outliers.contains(i))
|
||||
raw.append(GraphPoint(_distance.at(i), _time.at(i),
|
||||
_data.at(i).temperature()));
|
||||
}
|
||||
|
||||
return Graph(raw);
|
||||
return raw;
|
||||
}
|
||||
|
||||
Graph Track::cadence() const
|
||||
{
|
||||
Graph raw;
|
||||
|
||||
for (int i = 0; i < _data.size(); i++)
|
||||
if (_data.at(i).hasCadence() && !_outliers.contains(i))
|
||||
raw.append(GraphPoint(_distance.at(i), _time.at(i),
|
||||
_data.at(i).cadence()));
|
||||
|
||||
return filter(raw, WINDOW_CF);
|
||||
}
|
||||
|
||||
Graph Track::power() const
|
||||
{
|
||||
Graph raw;
|
||||
|
||||
for (int i = 0; i < _data.size(); i++)
|
||||
if (_data.at(i).hasPower() && !_outliers.contains(i))
|
||||
raw.append(GraphPoint(_distance.at(i), _time.at(i),
|
||||
_data.at(i).power()));
|
||||
|
||||
return filter(raw, WINDOW_PF);
|
||||
}
|
||||
|
||||
qreal Track::distance() const
|
||||
@ -206,12 +217,9 @@ Path Track::track() const
|
||||
{
|
||||
Path ret;
|
||||
|
||||
for (int i = 0; i < _data.size(); i++) {
|
||||
if (_outliers.contains(i))
|
||||
continue;
|
||||
|
||||
ret.append(_data.at(i).coordinates());
|
||||
}
|
||||
for (int i = 0; i < _data.size(); i++)
|
||||
if (!_outliers.contains(i))
|
||||
ret.append(_data.at(i).coordinates());
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ public:
|
||||
Graph speed() const;
|
||||
Graph heartRate() const;
|
||||
Graph temperature() const;
|
||||
Graph cadence() const;
|
||||
Graph power() const;
|
||||
|
||||
qreal distance() const;
|
||||
qreal time() const;
|
||||
|
@ -10,9 +10,11 @@ class Trackpoint
|
||||
{
|
||||
public:
|
||||
Trackpoint()
|
||||
{_elevation = NAN; _speed = NAN; _heartRate = NAN; _temperature = NAN;}
|
||||
{_elevation = NAN; _speed = NAN; _heartRate = NAN; _temperature = NAN;
|
||||
_cadence = NAN; _power = NAN;}
|
||||
Trackpoint(const Coordinates &coordinates) : _coordinates(coordinates)
|
||||
{_elevation = NAN; _speed = NAN; _heartRate = NAN; _temperature = NAN;}
|
||||
{_elevation = NAN; _speed = NAN; _heartRate = NAN; _temperature = NAN;
|
||||
_cadence = NAN; _power = NAN;}
|
||||
|
||||
const Coordinates &coordinates() const {return _coordinates;}
|
||||
Coordinates &rcoordinates() {return _coordinates;}
|
||||
@ -21,6 +23,8 @@ public:
|
||||
qreal speed() const {return _speed;}
|
||||
qreal heartRate() const {return _heartRate;}
|
||||
qreal temperature() const {return _temperature;}
|
||||
qreal cadence() const {return _cadence;}
|
||||
qreal power() const {return _power;}
|
||||
|
||||
void setCoordinates(const Coordinates &coordinates)
|
||||
{_coordinates = coordinates;}
|
||||
@ -29,12 +33,16 @@ public:
|
||||
void setSpeed(qreal speed) {_speed = speed;}
|
||||
void setHeartRate(qreal heartRate) {_heartRate = heartRate;}
|
||||
void setTemperature(qreal temperature) {_temperature = temperature;}
|
||||
void setCadence(qreal cadence) {_cadence = cadence;}
|
||||
void setPower(qreal power) {_power = power;}
|
||||
|
||||
bool hasTimestamp() const {return !_timestamp.isNull();}
|
||||
bool hasElevation() const {return !std::isnan(_elevation);}
|
||||
bool hasSpeed() const {return !std::isnan(_speed);}
|
||||
bool hasHeartRate() const {return !std::isnan(_heartRate);}
|
||||
bool hasTemperature() const {return !std::isnan(_temperature);}
|
||||
bool hasCadence() const {return !std::isnan(_cadence);}
|
||||
bool hasPower() const {return !std::isnan(_power);}
|
||||
|
||||
private:
|
||||
Coordinates _coordinates;
|
||||
@ -43,6 +51,8 @@ private:
|
||||
qreal _speed;
|
||||
qreal _heartRate;
|
||||
qreal _temperature;
|
||||
qreal _cadence;
|
||||
qreal _power;
|
||||
};
|
||||
|
||||
QDebug operator<<(QDebug dbg, const Trackpoint &trackpoint);
|
||||
|
Reference in New Issue
Block a user