Materials: External Modules Part 1
Refactored code to support local and external material sources This is the first PR in a series to support external modules. External modules allow materials to be stored in external data sources such as databases or web services. No new functionality is introduced in this PR, rather it is a refactoring of code that will allow for changes to be introduced in future PRs. Minor performance improvements have also been made in the model and material managers. The Python API has been enhanced for many data types to allow for modification within Python.
This commit is contained in:
committed by
Chris Hennes
parent
3c4977a2d4
commit
00c57a9d08
@@ -102,6 +102,44 @@ bool MaterialValue::operator==(const MaterialValue& other) const
|
||||
return (_valueType == other._valueType) && (_value == other._value);
|
||||
}
|
||||
|
||||
void MaterialValue::validate(const MaterialValue& other) const
|
||||
{
|
||||
if (_valueType != other._valueType) {
|
||||
throw InvalidProperty("Material property value types don't match");
|
||||
}
|
||||
if (_valueType == Quantity) {
|
||||
auto q1 = _value.value<Base::Quantity>();
|
||||
auto q2 = other._value.value<Base::Quantity>();
|
||||
if (q1.isValid()) {
|
||||
if (!q2.isValid()) {
|
||||
throw InvalidProperty("Invalid remote Material property quantity value");
|
||||
}
|
||||
if (q1.getUserString() != q2.getUserString()) {
|
||||
// Direct comparisons of the quantities may have precision issues
|
||||
// throw InvalidProperty("Material property quantity values don't match");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (q2.isValid()) {
|
||||
throw InvalidProperty("Remote Material property quantity should not have a value");
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (_valueType == Array2D) {
|
||||
auto a1 = static_cast<const Materials::Array2D*>(this);
|
||||
auto a2 = static_cast<const Materials::Array2D*>(&other);
|
||||
a1->validate(*a2);
|
||||
}
|
||||
else if (_valueType == Array3D) {
|
||||
auto a1 = static_cast<const Materials::Array3D*>(this);
|
||||
auto a2 = static_cast<const Materials::Array3D*>(&other);
|
||||
a1->validate(*a2);
|
||||
}
|
||||
else if (!(_value.isNull() && other._value.isNull()) && (_value != other._value)) {
|
||||
throw InvalidProperty("Material property values don't match");
|
||||
}
|
||||
}
|
||||
|
||||
QString MaterialValue::escapeString(const QString& source)
|
||||
{
|
||||
QString res = source;
|
||||
@@ -205,6 +243,11 @@ void MaterialValue::setList(const QList<QVariant>& value)
|
||||
}
|
||||
|
||||
bool MaterialValue::isNull() const
|
||||
{
|
||||
return isEmpty();
|
||||
}
|
||||
|
||||
bool MaterialValue::isEmpty() const
|
||||
{
|
||||
if (_value.isNull()) {
|
||||
return true;
|
||||
@@ -317,24 +360,24 @@ const Base::QuantityFormat MaterialValue::getQuantityFormat()
|
||||
|
||||
//===
|
||||
|
||||
TYPESYSTEM_SOURCE(Materials::Material2DArray, Materials::MaterialValue)
|
||||
TYPESYSTEM_SOURCE(Materials::Array2D, Materials::MaterialValue)
|
||||
|
||||
Material2DArray::Material2DArray()
|
||||
: MaterialValue(Array2D, Array2D)
|
||||
Array2D::Array2D()
|
||||
: MaterialValue(MaterialValue::Array2D, MaterialValue::Array2D)
|
||||
, _columns(0)
|
||||
{
|
||||
// Initialize separatelt to prevent recursion
|
||||
// setType(Array2D);
|
||||
}
|
||||
|
||||
Material2DArray::Material2DArray(const Material2DArray& other)
|
||||
Array2D::Array2D(const Array2D& other)
|
||||
: MaterialValue(other)
|
||||
, _columns(other._columns)
|
||||
{
|
||||
deepCopy(other);
|
||||
}
|
||||
|
||||
Material2DArray& Material2DArray::operator=(const Material2DArray& other)
|
||||
Array2D& Array2D::operator=(const Array2D& other)
|
||||
{
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
@@ -348,7 +391,7 @@ Material2DArray& Material2DArray::operator=(const Material2DArray& other)
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Material2DArray::deepCopy(const Material2DArray& other)
|
||||
void Array2D::deepCopy(const Array2D& other)
|
||||
{
|
||||
// Deep copy
|
||||
for (auto& row : other._rows) {
|
||||
@@ -361,26 +404,55 @@ void Material2DArray::deepCopy(const Material2DArray& other)
|
||||
}
|
||||
}
|
||||
|
||||
bool Material2DArray::isNull() const
|
||||
bool Array2D::isNull() const
|
||||
{
|
||||
return isEmpty();
|
||||
}
|
||||
|
||||
bool Array2D::isEmpty() const
|
||||
{
|
||||
return rows() <= 0;
|
||||
}
|
||||
|
||||
void Material2DArray::validateRow(int row) const
|
||||
void Array2D::validateRow(int row) const
|
||||
{
|
||||
if (row < 0 || row >= rows()) {
|
||||
throw InvalidIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void Material2DArray::validateColumn(int column) const
|
||||
void Array2D::validateColumn(int column) const
|
||||
{
|
||||
if (column < 0 || column >= columns()) {
|
||||
throw InvalidIndex();
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<QVariant>> Material2DArray::getRow(int row) const
|
||||
void Array2D::validate(const Array2D& other) const
|
||||
{
|
||||
if (rows() != other.rows()) {
|
||||
Base::Console().Log("Local row count %d, remote %d\n", rows(), other.rows());
|
||||
throw InvalidProperty("Material property value row counts don't match");
|
||||
}
|
||||
if (columns() != other.columns()) {
|
||||
Base::Console().Log("Local column count %d, remote %d\n", columns(), other.columns());
|
||||
throw InvalidProperty("Material property value column counts don't match");
|
||||
}
|
||||
try {
|
||||
for (int i = 0; i < rows(); i++) {
|
||||
for (int j = 0; j < columns(); j++) {
|
||||
if (getValue(i, j) != other.getValue(i, j)) {
|
||||
throw InvalidProperty("Material property values don't match");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const InvalidIndex&) {
|
||||
throw InvalidProperty("Material property value invalid array index");
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<QVariant>> Array2D::getRow(int row) const
|
||||
{
|
||||
validateRow(row);
|
||||
|
||||
@@ -392,7 +464,7 @@ std::shared_ptr<QList<QVariant>> Material2DArray::getRow(int row) const
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<QVariant>> Material2DArray::getRow(int row)
|
||||
std::shared_ptr<QList<QVariant>> Array2D::getRow(int row)
|
||||
{
|
||||
validateRow(row);
|
||||
|
||||
@@ -404,17 +476,17 @@ std::shared_ptr<QList<QVariant>> Material2DArray::getRow(int row)
|
||||
}
|
||||
}
|
||||
|
||||
void Material2DArray::addRow(const std::shared_ptr<QList<QVariant>>& row)
|
||||
void Array2D::addRow(const std::shared_ptr<QList<QVariant>>& row)
|
||||
{
|
||||
_rows.push_back(row);
|
||||
}
|
||||
|
||||
void Material2DArray::insertRow(int index, const std::shared_ptr<QList<QVariant>>& row)
|
||||
void Array2D::insertRow(int index, const std::shared_ptr<QList<QVariant>>& row)
|
||||
{
|
||||
_rows.insert(_rows.begin() + index, row);
|
||||
}
|
||||
|
||||
void Material2DArray::deleteRow(int row)
|
||||
void Array2D::deleteRow(int row)
|
||||
{
|
||||
if (row >= static_cast<int>(_rows.size()) || row < 0) {
|
||||
throw InvalidIndex();
|
||||
@@ -422,7 +494,18 @@ void Material2DArray::deleteRow(int row)
|
||||
_rows.erase(_rows.begin() + row);
|
||||
}
|
||||
|
||||
void Material2DArray::setValue(int row, int column, const QVariant& value)
|
||||
void Array2D::setRows(int rowCount)
|
||||
{
|
||||
while (rows() < rowCount) {
|
||||
auto row = std::make_shared<QList<QVariant>>();
|
||||
for (int i = 0; i < columns(); i++) {
|
||||
row->append(QVariant());
|
||||
}
|
||||
addRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
void Array2D::setValue(int row, int column, const QVariant& value)
|
||||
{
|
||||
validateRow(row);
|
||||
validateColumn(column);
|
||||
@@ -436,7 +519,7 @@ void Material2DArray::setValue(int row, int column, const QVariant& value)
|
||||
}
|
||||
}
|
||||
|
||||
QVariant Material2DArray::getValue(int row, int column) const
|
||||
QVariant Array2D::getValue(int row, int column) const
|
||||
{
|
||||
validateColumn(column);
|
||||
|
||||
@@ -449,7 +532,7 @@ QVariant Material2DArray::getValue(int row, int column) const
|
||||
}
|
||||
}
|
||||
|
||||
void Material2DArray::dumpRow(const std::shared_ptr<QList<QVariant>>& row)
|
||||
void Array2D::dumpRow(const std::shared_ptr<QList<QVariant>>& row)
|
||||
{
|
||||
Base::Console().Log("row: ");
|
||||
for (auto& column : *row) {
|
||||
@@ -458,14 +541,14 @@ void Material2DArray::dumpRow(const std::shared_ptr<QList<QVariant>>& row)
|
||||
Base::Console().Log("\n");
|
||||
}
|
||||
|
||||
void Material2DArray::dump() const
|
||||
void Array2D::dump() const
|
||||
{
|
||||
for (auto& row : _rows) {
|
||||
dumpRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
QString Material2DArray::getYAMLString() const
|
||||
QString Array2D::getYAMLString() const
|
||||
{
|
||||
if (isNull()) {
|
||||
return QString();
|
||||
@@ -511,10 +594,10 @@ QString Material2DArray::getYAMLString() const
|
||||
|
||||
//===
|
||||
|
||||
TYPESYSTEM_SOURCE(Materials::Material3DArray, Materials::MaterialValue)
|
||||
TYPESYSTEM_SOURCE(Materials::Array3D, Materials::MaterialValue)
|
||||
|
||||
Material3DArray::Material3DArray()
|
||||
: MaterialValue(Array3D, Array3D)
|
||||
Array3D::Array3D()
|
||||
: MaterialValue(MaterialValue::Array3D, MaterialValue::Array3D)
|
||||
, _currentDepth(0)
|
||||
, _columns(0)
|
||||
{
|
||||
@@ -522,26 +605,71 @@ Material3DArray::Material3DArray()
|
||||
// setType(Array3D);
|
||||
}
|
||||
|
||||
bool Material3DArray::isNull() const
|
||||
Array3D::Array3D(const Array3D& other)
|
||||
: MaterialValue(other)
|
||||
, _currentDepth(other._currentDepth)
|
||||
, _columns(other._columns)
|
||||
{
|
||||
deepCopy(other);
|
||||
}
|
||||
|
||||
Array3D& Array3D::operator=(const Array3D& other)
|
||||
{
|
||||
if (this == &other) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
MaterialValue::operator=(other);
|
||||
_columns = other._columns;
|
||||
_currentDepth = other._currentDepth;
|
||||
|
||||
deepCopy(other);
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Array3D::deepCopy(const Array3D& other)
|
||||
{
|
||||
// Deep copy
|
||||
_rowMap.clear();
|
||||
for (auto& depthTable : other._rowMap) {
|
||||
auto depth = addDepth(depthTable.first);
|
||||
auto rows = depthTable.second;
|
||||
for (auto row : *rows) {
|
||||
auto newRow = std::make_shared<QList<Base::Quantity>>();
|
||||
for (auto column : *row) {
|
||||
newRow->append(column);
|
||||
}
|
||||
addRow(depth, newRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Array3D::isNull() const
|
||||
{
|
||||
return isEmpty();
|
||||
}
|
||||
|
||||
bool Array3D::isEmpty() const
|
||||
{
|
||||
return depth() <= 0;
|
||||
}
|
||||
|
||||
void Material3DArray::validateDepth(int level) const
|
||||
void Array3D::validateDepth(int level) const
|
||||
{
|
||||
if (level < 0 || level >= depth()) {
|
||||
throw InvalidIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void Material3DArray::validateColumn(int column) const
|
||||
void Array3D::validateColumn(int column) const
|
||||
{
|
||||
if (column < 0 || column >= columns()) {
|
||||
throw InvalidIndex();
|
||||
}
|
||||
}
|
||||
|
||||
void Material3DArray::validateRow(int level, int row) const
|
||||
void Array3D::validateRow(int level, int row) const
|
||||
{
|
||||
validateDepth(level);
|
||||
|
||||
@@ -550,8 +678,18 @@ void Material3DArray::validateRow(int level, int row) const
|
||||
}
|
||||
}
|
||||
|
||||
void Array3D::validate(const Array3D& other) const
|
||||
{
|
||||
if (depth() != other.depth()) {
|
||||
throw InvalidProperty("Material property value row counts don't match");
|
||||
}
|
||||
if (columns() != other.columns()) {
|
||||
throw InvalidProperty("Material property value column counts don't match");
|
||||
}
|
||||
}
|
||||
|
||||
const std::shared_ptr<QList<std::shared_ptr<QList<Base::Quantity>>>>&
|
||||
Material3DArray::getTable(const Base::Quantity& depth) const
|
||||
Array3D::getTable(const Base::Quantity& depth) const
|
||||
{
|
||||
for (auto& it : _rowMap) {
|
||||
if (std::get<0>(it) == depth) {
|
||||
@@ -563,7 +701,7 @@ Material3DArray::getTable(const Base::Quantity& depth) const
|
||||
}
|
||||
|
||||
const std::shared_ptr<QList<std::shared_ptr<QList<Base::Quantity>>>>&
|
||||
Material3DArray::getTable(int depthIndex) const
|
||||
Array3D::getTable(int depthIndex) const
|
||||
{
|
||||
try {
|
||||
return std::get<1>(_rowMap.at(depthIndex));
|
||||
@@ -573,7 +711,7 @@ Material3DArray::getTable(int depthIndex) const
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int depth, int row) const
|
||||
std::shared_ptr<QList<Base::Quantity>> Array3D::getRow(int depth, int row) const
|
||||
{
|
||||
validateRow(depth, row);
|
||||
|
||||
@@ -585,13 +723,13 @@ std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int depth, int ro
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int row) const
|
||||
std::shared_ptr<QList<Base::Quantity>> Array3D::getRow(int row) const
|
||||
{
|
||||
// Check if we can convert otherwise throw error
|
||||
return getRow(_currentDepth, row);
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int depth, int row)
|
||||
std::shared_ptr<QList<Base::Quantity>> Array3D::getRow(int depth, int row)
|
||||
{
|
||||
validateRow(depth, row);
|
||||
|
||||
@@ -603,12 +741,12 @@ std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int depth, int ro
|
||||
}
|
||||
}
|
||||
|
||||
std::shared_ptr<QList<Base::Quantity>> Material3DArray::getRow(int row)
|
||||
std::shared_ptr<QList<Base::Quantity>> Array3D::getRow(int row)
|
||||
{
|
||||
return getRow(_currentDepth, row);
|
||||
}
|
||||
|
||||
void Material3DArray::addRow(int depth, const std::shared_ptr<QList<Base::Quantity>>& row)
|
||||
void Array3D::addRow(int depth, const std::shared_ptr<QList<Base::Quantity>>& row)
|
||||
{
|
||||
try {
|
||||
getTable(depth)->push_back(row);
|
||||
@@ -618,12 +756,12 @@ void Material3DArray::addRow(int depth, const std::shared_ptr<QList<Base::Quanti
|
||||
}
|
||||
}
|
||||
|
||||
void Material3DArray::addRow(const std::shared_ptr<QList<Base::Quantity>>& row)
|
||||
void Array3D::addRow(const std::shared_ptr<QList<Base::Quantity>>& row)
|
||||
{
|
||||
addRow(_currentDepth, row);
|
||||
}
|
||||
|
||||
int Material3DArray::addDepth(int depth, const Base::Quantity& value)
|
||||
int Array3D::addDepth(int depth, const Base::Quantity& value)
|
||||
{
|
||||
if (depth == this->depth()) {
|
||||
// Append to the end
|
||||
@@ -639,7 +777,7 @@ int Material3DArray::addDepth(int depth, const Base::Quantity& value)
|
||||
return depth;
|
||||
}
|
||||
|
||||
int Material3DArray::addDepth(const Base::Quantity& value)
|
||||
int Array3D::addDepth(const Base::Quantity& value)
|
||||
{
|
||||
auto rowVector = std::make_shared<QList<std::shared_ptr<QList<Base::Quantity>>>>();
|
||||
auto entry = std::make_pair(value, rowVector);
|
||||
@@ -648,13 +786,22 @@ int Material3DArray::addDepth(const Base::Quantity& value)
|
||||
return depth() - 1;
|
||||
}
|
||||
|
||||
void Material3DArray::deleteDepth(int depth)
|
||||
void Array3D::deleteDepth(int depth)
|
||||
{
|
||||
deleteRows(depth); // This may throw an InvalidIndex
|
||||
_rowMap.erase(_rowMap.begin() + depth);
|
||||
}
|
||||
|
||||
void Material3DArray::insertRow(int depth,
|
||||
void Array3D::setDepth(int depthCount)
|
||||
{
|
||||
Base::Quantity dummy;
|
||||
dummy.setInvalid();
|
||||
while (depth() < depthCount) {
|
||||
addDepth(dummy);
|
||||
}
|
||||
}
|
||||
|
||||
void Array3D::insertRow(int depth,
|
||||
int row,
|
||||
const std::shared_ptr<QList<Base::Quantity>>& rowData)
|
||||
{
|
||||
@@ -667,12 +814,12 @@ void Material3DArray::insertRow(int depth,
|
||||
}
|
||||
}
|
||||
|
||||
void Material3DArray::insertRow(int row, const std::shared_ptr<QList<Base::Quantity>>& rowData)
|
||||
void Array3D::insertRow(int row, const std::shared_ptr<QList<Base::Quantity>>& rowData)
|
||||
{
|
||||
insertRow(_currentDepth, row, rowData);
|
||||
}
|
||||
|
||||
void Material3DArray::deleteRow(int depth, int row)
|
||||
void Array3D::deleteRow(int depth, int row)
|
||||
{
|
||||
auto table = getTable(depth);
|
||||
if (row >= static_cast<int>(table->size()) || row < 0) {
|
||||
@@ -681,23 +828,23 @@ void Material3DArray::deleteRow(int depth, int row)
|
||||
table->erase(table->begin() + row);
|
||||
}
|
||||
|
||||
void Material3DArray::deleteRow(int row)
|
||||
void Array3D::deleteRow(int row)
|
||||
{
|
||||
deleteRow(_currentDepth, row);
|
||||
}
|
||||
|
||||
void Material3DArray::deleteRows(int depth)
|
||||
void Array3D::deleteRows(int depth)
|
||||
{
|
||||
auto table = getTable(depth);
|
||||
table->clear();
|
||||
}
|
||||
|
||||
void Material3DArray::deleteRows()
|
||||
void Array3D::deleteRows()
|
||||
{
|
||||
deleteRows(_currentDepth);
|
||||
}
|
||||
|
||||
int Material3DArray::rows(int depth) const
|
||||
int Array3D::rows(int depth) const
|
||||
{
|
||||
if (depth < 0 || (depth == 0 && this->depth() == 0)) {
|
||||
return 0;
|
||||
@@ -707,7 +854,21 @@ int Material3DArray::rows(int depth) const
|
||||
return getTable(depth)->size();
|
||||
}
|
||||
|
||||
void Material3DArray::setValue(int depth, int row, int column, const Base::Quantity& value)
|
||||
void Array3D::setRows(int depth, int rowCount)
|
||||
{
|
||||
Base::Quantity dummy;
|
||||
dummy.setInvalid();
|
||||
|
||||
while (rows(depth) < rowCount) {
|
||||
auto row = std::make_shared<QList<Base::Quantity>>();
|
||||
for (int i = 0; i < columns(); i++) {
|
||||
row->append(dummy);
|
||||
}
|
||||
addRow(depth, row);
|
||||
}
|
||||
}
|
||||
|
||||
void Array3D::setValue(int depth, int row, int column, const Base::Quantity& value)
|
||||
{
|
||||
validateRow(depth, row);
|
||||
validateColumn(column);
|
||||
@@ -721,12 +882,12 @@ void Material3DArray::setValue(int depth, int row, int column, const Base::Quant
|
||||
}
|
||||
}
|
||||
|
||||
void Material3DArray::setValue(int row, int column, const Base::Quantity& value)
|
||||
void Array3D::setValue(int row, int column, const Base::Quantity& value)
|
||||
{
|
||||
setValue(_currentDepth, row, column, value);
|
||||
}
|
||||
|
||||
void Material3DArray::setDepthValue(int depth, const Base::Quantity& value)
|
||||
void Array3D::setDepthValue(int depth, const Base::Quantity& value)
|
||||
{
|
||||
try {
|
||||
auto oldRows = getTable(depth);
|
||||
@@ -737,13 +898,13 @@ void Material3DArray::setDepthValue(int depth, const Base::Quantity& value)
|
||||
}
|
||||
}
|
||||
|
||||
void Material3DArray::setDepthValue(const Base::Quantity& value)
|
||||
void Array3D::setDepthValue(const Base::Quantity& value)
|
||||
{
|
||||
setDepthValue(_currentDepth, value);
|
||||
}
|
||||
|
||||
|
||||
Base::Quantity Material3DArray::getValue(int depth, int row, int column) const
|
||||
Base::Quantity Array3D::getValue(int depth, int row, int column) const
|
||||
{
|
||||
// getRow validates depth and row. Do that first
|
||||
auto val = getRow(depth, row);
|
||||
@@ -757,12 +918,12 @@ Base::Quantity Material3DArray::getValue(int depth, int row, int column) const
|
||||
}
|
||||
}
|
||||
|
||||
Base::Quantity Material3DArray::getValue(int row, int column) const
|
||||
Base::Quantity Array3D::getValue(int row, int column) const
|
||||
{
|
||||
return getValue(_currentDepth, row, column);
|
||||
}
|
||||
|
||||
Base::Quantity Material3DArray::getDepthValue(int depth) const
|
||||
Base::Quantity Array3D::getDepthValue(int depth) const
|
||||
{
|
||||
validateDepth(depth);
|
||||
|
||||
@@ -774,12 +935,12 @@ Base::Quantity Material3DArray::getDepthValue(int depth) const
|
||||
}
|
||||
}
|
||||
|
||||
int Material3DArray::currentDepth() const
|
||||
int Array3D::currentDepth() const
|
||||
{
|
||||
return _currentDepth;
|
||||
}
|
||||
|
||||
void Material3DArray::setCurrentDepth(int depth)
|
||||
void Array3D::setCurrentDepth(int depth)
|
||||
{
|
||||
validateDepth(depth);
|
||||
|
||||
@@ -794,7 +955,7 @@ void Material3DArray::setCurrentDepth(int depth)
|
||||
}
|
||||
}
|
||||
|
||||
QString Material3DArray::getYAMLString() const
|
||||
QString Array3D::getYAMLString() const
|
||||
{
|
||||
if (isNull()) {
|
||||
return QString();
|
||||
|
||||
Reference in New Issue
Block a user