Remove GeoJSON based debugging output, we can now generate vector tiles with roughly the same data on-the-fly.
This commit is contained in:
parent
ae802a8a83
commit
56e35e8ef2
@ -30,7 +30,6 @@ if(WIN32 AND MSVC_VERSION LESS 1800)
|
||||
endif()
|
||||
|
||||
option(ENABLE_JSON_LOGGING "Adds additional JSON debug logging to the response" OFF)
|
||||
option(DEBUG_GEOMETRY "Enables an option to dump GeoJSON of the final routing graph" OFF)
|
||||
option(BUILD_TOOLS "Build OSRM tools" OFF)
|
||||
option(ENABLE_ASSERTIONS OFF)
|
||||
|
||||
@ -278,12 +277,6 @@ if (ENABLE_JSON_LOGGING)
|
||||
add_definitions(-DENABLE_JSON_LOGGING)
|
||||
endif()
|
||||
|
||||
if (DEBUG_GEOMETRY)
|
||||
message(STATUS "Enabling final edge weight GeoJSON output option")
|
||||
add_definitions(-DDEBUG_GEOMETRY)
|
||||
endif()
|
||||
|
||||
|
||||
# Binaries
|
||||
target_link_libraries(osrm-datastore osrm_store ${Boost_LIBRARIES})
|
||||
target_link_libraries(osrm-extract osrm_extract ${Boost_LIBRARIES})
|
||||
|
@ -50,20 +50,11 @@ class EdgeBasedGraphFactory
|
||||
const std::vector<QueryNode> &node_info_list,
|
||||
SpeedProfileProperties speed_profile);
|
||||
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
void Run(const std::string &original_edge_data_filename,
|
||||
lua_State *lua_state,
|
||||
const std::string &edge_segment_lookup_filename,
|
||||
const std::string &edge_penalty_filename,
|
||||
const bool generate_edge_lookup,
|
||||
const std::string &debug_turns_path);
|
||||
#else
|
||||
void Run(const std::string &original_edge_data_filename,
|
||||
lua_State *lua_state,
|
||||
const std::string &edge_segment_lookup_filename,
|
||||
const std::string &edge_penalty_filename,
|
||||
const bool generate_edge_lookup);
|
||||
#endif
|
||||
|
||||
// The following get access functions destroy the content in the factory
|
||||
void GetEdgeBasedEdges(util::DeallocatingVector<EdgeBasedEdge> &edges);
|
||||
|
@ -68,9 +68,6 @@ struct ExtractorConfig
|
||||
bool generate_edge_lookup;
|
||||
std::string edge_penalty_path;
|
||||
std::string edge_segment_lookup_path;
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
std::string debug_turns_path;
|
||||
#endif
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,242 +0,0 @@
|
||||
#ifndef DEBUG_GEOMETRY_H
|
||||
#define DEBUG_GEOMETRY_H
|
||||
|
||||
#include "contractor/contractor_config.hpp"
|
||||
#include "extractor/query_node.hpp"
|
||||
#include "osrm/coordinate.hpp"
|
||||
#include <boost/filesystem/fstream.hpp>
|
||||
|
||||
#ifndef DEBUG_GEOMETRY
|
||||
|
||||
namespace osrm
|
||||
{
|
||||
namespace util
|
||||
{
|
||||
|
||||
inline void DEBUG_GEOMETRY_START(const contractor::ContractorConfig & /* config */) {}
|
||||
inline void DEBUG_GEOMETRY_EDGE(int /* new_segment_weight */,
|
||||
double /* segment_length */,
|
||||
OSMNodeID /* previous_osm_node_id */,
|
||||
OSMNodeID /* this_osm_node_id */)
|
||||
{
|
||||
}
|
||||
inline void DEBUG_GEOMETRY_STOP() {}
|
||||
|
||||
inline void DEBUG_TURNS_START(const std::string & /* debug_turns_filename */) {}
|
||||
inline void DEBUG_TURN(const NodeID /* node */,
|
||||
const std::vector<extractor::QueryNode> & /* m_node_info_list */,
|
||||
const FixedPointCoordinate /* first_coordinate */,
|
||||
const int /* turn_angle */,
|
||||
const int /* turn_penalty */)
|
||||
{
|
||||
}
|
||||
inline void DEBUG_UTURN(const NodeID /* node */,
|
||||
const std::vector<extractor::QueryNode> & /* m_node_info_list */,
|
||||
const int /* uturn_penalty */)
|
||||
{
|
||||
}
|
||||
inline void DEBUG_SIGNAL(const NodeID /* node */,
|
||||
const std::vector<extractor::QueryNode> & /* m_node_info_list */,
|
||||
const int /* signal_penalty */)
|
||||
{
|
||||
}
|
||||
|
||||
inline void DEBUG_TURNS_STOP() {}
|
||||
}
|
||||
}
|
||||
|
||||
#else // DEBUG_GEOMETRY
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
|
||||
#include "util/coordinate.hpp"
|
||||
#include "util/coordinate_calculation.hpp"
|
||||
|
||||
namespace osrm
|
||||
{
|
||||
namespace util
|
||||
{
|
||||
|
||||
boost::filesystem::ofstream debug_geometry_file;
|
||||
bool dg_output_debug_geometry = false;
|
||||
bool dg_first_debug_geometry = true;
|
||||
char dg_time_buffer[80];
|
||||
|
||||
boost::filesystem::ofstream dg_debug_turns_file;
|
||||
bool dg_output_turn_debug = false;
|
||||
bool dg_first_turn_debug = true;
|
||||
|
||||
std::unordered_map<OSMNodeID, util::FixedPointCoordinate> node_lookup_map;
|
||||
|
||||
inline void DEBUG_GEOMETRY_START(const contractor::ContractorConfig &config)
|
||||
{
|
||||
time_t raw_time;
|
||||
struct tm *timeinfo;
|
||||
time(&raw_time);
|
||||
timeinfo = localtime(&raw_time);
|
||||
strftime(dg_time_buffer, 80, "%Y-%m-%d %H:%M %Z", timeinfo);
|
||||
|
||||
boost::filesystem::ifstream nodes_input_stream{config.node_based_graph_path,
|
||||
std::ios_base::in | std::ios_base::binary};
|
||||
|
||||
extractor::QueryNode current_node;
|
||||
unsigned number_of_coordinates = 0;
|
||||
nodes_input_stream.read((char *)&number_of_coordinates, sizeof(unsigned));
|
||||
|
||||
for (unsigned i = 0; i < number_of_coordinates; ++i)
|
||||
{
|
||||
nodes_input_stream.read((char *)¤t_node, sizeof(extractor::QueryNode));
|
||||
node_lookup_map[current_node.node_id] =
|
||||
util::FixedPointCoordinate(current_node.lat, current_node.lon);
|
||||
}
|
||||
nodes_input_stream.close();
|
||||
|
||||
dg_output_debug_geometry = config.debug_geometry_path != "";
|
||||
|
||||
if (dg_output_debug_geometry)
|
||||
{
|
||||
debug_geometry_file.open(config.debug_geometry_path, std::ios::binary);
|
||||
debug_geometry_file << "{\"type\":\"FeatureCollection\", \"features\":[\n";
|
||||
debug_geometry_file << std::setprecision(10);
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_GEOMETRY_EDGE(int new_segment_weight,
|
||||
double segment_length,
|
||||
OSMNodeID previous_osm_node_id,
|
||||
OSMNodeID this_osm_node_id)
|
||||
{
|
||||
if (dg_output_debug_geometry)
|
||||
{
|
||||
if (!dg_first_debug_geometry)
|
||||
{
|
||||
debug_geometry_file << ",\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
debug_geometry_file << "\n";
|
||||
dg_first_debug_geometry = false;
|
||||
}
|
||||
debug_geometry_file << "{ \"type\":\"Feature\",\"properties\":{\"original\":false, "
|
||||
"\"weight\":"
|
||||
<< new_segment_weight / 10.0 << ",\"speed\":"
|
||||
<< static_cast<int>(
|
||||
std::floor((segment_length / new_segment_weight) * 10. * 3.6))
|
||||
<< ",";
|
||||
debug_geometry_file << "\"from_node\": " << OSMNodeID_to_uint64_t(previous_osm_node_id)
|
||||
<< ", \"to_node\": " << OSMNodeID_to_uint64_t(this_osm_node_id) << ",";
|
||||
debug_geometry_file << "\"timestamp\": \"" << dg_time_buffer << "\"},";
|
||||
debug_geometry_file
|
||||
<< "\"geometry\":{\"type\":\"LineString\",\"coordinates\":[["
|
||||
<< node_lookup_map[previous_osm_node_id].lon / osrm::COORDINATE_PRECISION << ","
|
||||
<< node_lookup_map[previous_osm_node_id].lat / osrm::COORDINATE_PRECISION << "],["
|
||||
<< node_lookup_map[this_osm_node_id].lon / osrm::COORDINATE_PRECISION << ","
|
||||
<< node_lookup_map[this_osm_node_id].lat / osrm::COORDINATE_PRECISION << "]]}}";
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_GEOMETRY_STOP()
|
||||
{
|
||||
if (dg_output_debug_geometry)
|
||||
{
|
||||
debug_geometry_file << "\n]}" << "\n";
|
||||
debug_geometry_file.close();
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_TURNS_START(const std::string &debug_turns_path)
|
||||
{
|
||||
dg_output_turn_debug = debug_turns_path != "";
|
||||
if (dg_output_turn_debug)
|
||||
{
|
||||
dg_debug_turns_file.open(debug_turns_path);
|
||||
dg_debug_turns_file << "{\"type\":\"FeatureCollection\", \"features\":[" << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_SIGNAL(const NodeID node,
|
||||
const std::vector<extractor::QueryNode> &m_node_info_list,
|
||||
const int traffic_signal_penalty)
|
||||
{
|
||||
if (dg_output_turn_debug)
|
||||
{
|
||||
const extractor::QueryNode &nodeinfo = m_node_info_list[node];
|
||||
if (!dg_first_turn_debug)
|
||||
dg_debug_turns_file << "," << "\n";
|
||||
dg_debug_turns_file
|
||||
<< "{ \"type\":\"Feature\",\"properties\":{\"type\":\"trafficlights\",\"cost\":"
|
||||
<< traffic_signal_penalty / 10. << "},";
|
||||
dg_debug_turns_file << " \"geometry\":{\"type\":\"Point\",\"coordinates\":["
|
||||
<< std::setprecision(12) << nodeinfo.lon / COORDINATE_PRECISION << ","
|
||||
<< nodeinfo.lat / COORDINATE_PRECISION << "]}}";
|
||||
dg_first_turn_debug = false;
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_UTURN(const NodeID node,
|
||||
const std::vector<extractor::QueryNode> &m_node_info_list,
|
||||
const int traffic_signal_penalty)
|
||||
{
|
||||
if (dg_output_turn_debug)
|
||||
{
|
||||
const extractor::QueryNode &nodeinfo = m_node_info_list[node];
|
||||
if (!dg_first_turn_debug)
|
||||
dg_debug_turns_file << "," << "\n";
|
||||
dg_debug_turns_file
|
||||
<< "{ \"type\":\"Feature\",\"properties\":{\"type\":\"trafficlights\",\"cost\":"
|
||||
<< traffic_signal_penalty / 10. << "},";
|
||||
dg_debug_turns_file << " \"geometry\":{\"type\":\"Point\",\"coordinates\":["
|
||||
<< std::setprecision(12) << nodeinfo.lon / COORDINATE_PRECISION << ","
|
||||
<< nodeinfo.lat / COORDINATE_PRECISION << "]}}";
|
||||
dg_first_turn_debug = false;
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_TURN(const NodeID node,
|
||||
const std::vector<extractor::QueryNode> &m_node_info_list,
|
||||
const FixedPointCoordinate first_coordinate,
|
||||
const int turn_angle,
|
||||
const int turn_penalty)
|
||||
{
|
||||
if (turn_penalty > 0 && dg_output_turn_debug)
|
||||
{
|
||||
const extractor::QueryNode &v = m_node_info_list[node];
|
||||
|
||||
const float bearing_uv = coordinate_calculation::bearing(first_coordinate, v);
|
||||
float uvw_normal = bearing_uv + turn_angle / 2;
|
||||
while (uvw_normal >= 360.)
|
||||
{
|
||||
uvw_normal -= 360.;
|
||||
}
|
||||
|
||||
if (!dg_first_turn_debug)
|
||||
dg_debug_turns_file << "," << "\n";
|
||||
dg_debug_turns_file << "{ \"type\":\"Feature\",\"properties\":{\"type\":\"turn\",\"cost\":"
|
||||
<< turn_penalty / 10.
|
||||
<< ",\"turn_angle\":" << static_cast<int>(turn_angle)
|
||||
<< ",\"normal\":" << static_cast<int>(uvw_normal) << "},";
|
||||
dg_debug_turns_file << " \"geometry\":{\"type\":\"Point\",\"coordinates\":["
|
||||
<< std::setprecision(12) << v.lon / COORDINATE_PRECISION << ","
|
||||
<< v.lat / COORDINATE_PRECISION << "]}}";
|
||||
dg_first_turn_debug = false;
|
||||
}
|
||||
}
|
||||
|
||||
inline void DEBUG_TURNS_STOP()
|
||||
{
|
||||
if (dg_output_turn_debug)
|
||||
{
|
||||
dg_debug_turns_file << "\n]}" << "\n";
|
||||
dg_debug_turns_file.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // DEBUG_GEOMETRY
|
||||
|
||||
#endif // DEBUG_GEOMETRY_H
|
@ -1,32 +0,0 @@
|
||||
#ifndef TILE_BBOX
|
||||
#define TILE_BBOX
|
||||
|
||||
#include "util/rectangle.hpp"
|
||||
#include <cmath>
|
||||
|
||||
namespace osrm
|
||||
{
|
||||
namespace util
|
||||
{
|
||||
|
||||
inline RectangleInt2D TileToBBOX(int z, int x, int y)
|
||||
{
|
||||
double minx = x / pow(2.0, z) * 360 - 180;
|
||||
double n = M_PI - 2.0 * M_PI * y / pow(2.0, z);
|
||||
double miny = 180.0 / M_PI * atan(0.5 * (exp(n) - exp(-n)));
|
||||
|
||||
double maxx = (x + 1) / pow(2.0, z) * 360 - 180;
|
||||
double mn = M_PI - 2.0 * M_PI * (y + 1) / pow(2.0, z);
|
||||
double maxy = 180.0 / M_PI * atan(0.5 * (exp(mn) - exp(-mn)));
|
||||
|
||||
return {
|
||||
static_cast<int32_t>(std::min(minx,maxx) * COORDINATE_PRECISION),
|
||||
static_cast<int32_t>(std::max(minx,maxx) * COORDINATE_PRECISION),
|
||||
static_cast<int32_t>(std::min(miny,maxy) * COORDINATE_PRECISION),
|
||||
static_cast<int32_t>(std::min(miny,maxy) * COORDINATE_PRECISION)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
@ -32,8 +32,6 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "util/debug_geometry.hpp"
|
||||
|
||||
namespace std
|
||||
{
|
||||
|
||||
@ -188,8 +186,6 @@ std::size_t Contractor::LoadEdgeExpandedGraph(
|
||||
}
|
||||
}
|
||||
|
||||
util::DEBUG_GEOMETRY_START(config);
|
||||
|
||||
// TODO: can we read this in bulk? util::DeallocatingVector isn't necessarily
|
||||
// all stored contiguously
|
||||
for (; number_of_edges > 0; --number_of_edges)
|
||||
@ -235,17 +231,11 @@ std::size_t Contractor::LoadEdgeExpandedGraph(
|
||||
std::max(1, static_cast<int>(std::floor(
|
||||
(segment_length * 10.) / (speed_iter->second / 3.6) + .5)));
|
||||
new_weight += new_segment_weight;
|
||||
|
||||
util::DEBUG_GEOMETRY_EDGE(new_segment_weight, segment_length,
|
||||
previous_osm_node_id, this_osm_node_id);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no lookup found, use the original weight value for this segment
|
||||
new_weight += segment_weight;
|
||||
|
||||
util::DEBUG_GEOMETRY_EDGE(segment_weight, segment_length, previous_osm_node_id,
|
||||
this_osm_node_id);
|
||||
}
|
||||
|
||||
previous_osm_node_id = this_osm_node_id;
|
||||
@ -257,7 +247,6 @@ std::size_t Contractor::LoadEdgeExpandedGraph(
|
||||
edge_based_edge_list.emplace_back(std::move(inbuffer));
|
||||
}
|
||||
|
||||
util::DEBUG_GEOMETRY_STOP();
|
||||
util::SimpleLogger().Write() << "Done reading edges";
|
||||
return max_edge_id;
|
||||
}
|
||||
|
@ -9,8 +9,6 @@
|
||||
#include "util/timing_util.hpp"
|
||||
#include "util/exception.hpp"
|
||||
|
||||
#include "util/debug_geometry.hpp"
|
||||
|
||||
#include <boost/assert.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
@ -244,20 +242,11 @@ void EdgeBasedGraphFactory::FlushVectorToStream(
|
||||
original_edge_data_vector.clear();
|
||||
}
|
||||
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
void EdgeBasedGraphFactory::Run(const std::string &original_edge_data_filename,
|
||||
lua_State *lua_state,
|
||||
const std::string &edge_segment_lookup_filename,
|
||||
const std::string &edge_penalty_filename,
|
||||
const bool generate_edge_lookup,
|
||||
const std::string &debug_turns_path)
|
||||
#else
|
||||
void EdgeBasedGraphFactory::Run(const std::string &original_edge_data_filename,
|
||||
lua_State *lua_state,
|
||||
const std::string &edge_segment_lookup_filename,
|
||||
const std::string &edge_penalty_filename,
|
||||
const bool generate_edge_lookup)
|
||||
#endif
|
||||
{
|
||||
TIMER_START(renumber);
|
||||
m_max_edge_id = RenumberEdges() - 1;
|
||||
@ -269,13 +258,8 @@ void EdgeBasedGraphFactory::Run(const std::string &original_edge_data_filename,
|
||||
TIMER_STOP(generate_nodes);
|
||||
|
||||
TIMER_START(generate_edges);
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
GenerateEdgeExpandedEdges(original_edge_data_filename, lua_state, edge_segment_lookup_filename,
|
||||
edge_penalty_filename, generate_edge_lookup, debug_turns_path);
|
||||
#else
|
||||
GenerateEdgeExpandedEdges(original_edge_data_filename, lua_state, edge_segment_lookup_filename,
|
||||
edge_penalty_filename, generate_edge_lookup);
|
||||
#endif
|
||||
|
||||
TIMER_STOP(generate_edges);
|
||||
|
||||
@ -367,22 +351,12 @@ void EdgeBasedGraphFactory::GenerateEdgeExpandedNodes()
|
||||
}
|
||||
|
||||
/// Actually it also generates OriginalEdgeData and serializes them...
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
void EdgeBasedGraphFactory::GenerateEdgeExpandedEdges(
|
||||
const std::string &original_edge_data_filename,
|
||||
lua_State *lua_state,
|
||||
const std::string &edge_segment_lookup_filename,
|
||||
const std::string &edge_fixed_penalties_filename,
|
||||
const bool generate_edge_lookup,
|
||||
const std::string &debug_turns_path)
|
||||
#else
|
||||
void EdgeBasedGraphFactory::GenerateEdgeExpandedEdges(
|
||||
const std::string &original_edge_data_filename,
|
||||
lua_State *lua_state,
|
||||
const std::string &edge_segment_lookup_filename,
|
||||
const std::string &edge_fixed_penalties_filename,
|
||||
const bool generate_edge_lookup)
|
||||
#endif
|
||||
{
|
||||
util::SimpleLogger().Write() << "generating edge-expanded edges";
|
||||
|
||||
@ -414,10 +388,6 @@ void EdgeBasedGraphFactory::GenerateEdgeExpandedEdges(
|
||||
// linear number of turns only.
|
||||
util::Percent progress(m_node_based_graph->GetNumberOfNodes());
|
||||
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
util::DEBUG_TURNS_START(debug_turns_path);
|
||||
#endif
|
||||
|
||||
for (const auto node_u : util::irange(0u, m_node_based_graph->GetNumberOfNodes()))
|
||||
{
|
||||
// progress.printStatus(node_u);
|
||||
@ -455,9 +425,6 @@ void EdgeBasedGraphFactory::GenerateEdgeExpandedEdges(
|
||||
if (m_traffic_lights.find(node_v) != m_traffic_lights.end())
|
||||
{
|
||||
distance += speed_profile.traffic_signal_penalty;
|
||||
|
||||
util::DEBUG_SIGNAL(node_v, m_node_info_list,
|
||||
speed_profile.traffic_signal_penalty);
|
||||
}
|
||||
|
||||
const int turn_penalty = GetTurnPenalty(turn_angle, lua_state);
|
||||
@ -466,8 +433,6 @@ void EdgeBasedGraphFactory::GenerateEdgeExpandedEdges(
|
||||
if (turn_instruction == TurnInstruction::UTurn)
|
||||
{
|
||||
distance += speed_profile.u_turn_penalty;
|
||||
|
||||
util::DEBUG_UTURN(node_v, m_node_info_list, speed_profile.u_turn_penalty);
|
||||
}
|
||||
|
||||
distance += turn_penalty;
|
||||
@ -575,8 +540,6 @@ void EdgeBasedGraphFactory::GenerateEdgeExpandedEdges(
|
||||
}
|
||||
}
|
||||
|
||||
util::DEBUG_TURNS_STOP();
|
||||
|
||||
FlushVectorToStream(edge_data_file, original_edge_data_vector);
|
||||
|
||||
edge_data_file.seekp(std::ios::beg);
|
||||
|
@ -46,13 +46,6 @@ return_code parseArguments(int argc, char *argv[], contractor::ContractorConfig
|
||||
->default_value(false),
|
||||
"Use .level file to retain the contaction level for each node from the last run.");
|
||||
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
config_options.add_options()(
|
||||
"debug-geometry",
|
||||
boost::program_options::value<std::string>(&contractor_config.debug_geometry_path),
|
||||
"Write out edge-weight debugging geometry data in GeoJSON format to this file");
|
||||
#endif
|
||||
|
||||
// hidden options, will be allowed on command line, but will not be shown to the user
|
||||
boost::program_options::options_description hidden_options("Hidden options");
|
||||
hidden_options.add_options()("input,i", boost::program_options::value<boost::filesystem::path>(
|
||||
|
@ -49,12 +49,6 @@ return_code parseArguments(int argc, char *argv[], extractor::ExtractorConfig &e
|
||||
"Number of nodes required before a strongly-connected-componennt is considered big "
|
||||
"(affects nearest neighbor snapping)");
|
||||
|
||||
#ifdef DEBUG_GEOMETRY
|
||||
config_options.add_options()("debug-turns", boost::program_options::value<std::string>(
|
||||
&extractor_config.debug_turns_path),
|
||||
"Write out GeoJSON with turn penalty data");
|
||||
#endif // DEBUG_GEOMETRY
|
||||
|
||||
// hidden options, will be allowed on command line, but will not be
|
||||
// shown to the user
|
||||
boost::program_options::options_description hidden_options("Hidden options");
|
||||
|
Loading…
Reference in New Issue
Block a user