Symbolizers with method for creating literals
This commit is contained in:
@@ -25,6 +25,24 @@ ol.Feature = function(opt_geometry, opt_values) {
|
||||
goog.inherits(ol.Feature, ol.Object);
|
||||
|
||||
|
||||
/**
|
||||
* @return {Object} Attributes object.
|
||||
*/
|
||||
ol.Feature.prototype.getAttributes = function() {
|
||||
// TODO: see https://github.com/openlayers/ol3/pull/217
|
||||
// var keys = this.getKeys(),
|
||||
// len = keys.length,
|
||||
// attributes = {},
|
||||
// i, key
|
||||
// for (var i = 0; i < len; ++ i) {
|
||||
// key = keys[i];
|
||||
// attributes[key] = this.get(key);
|
||||
// }
|
||||
// return attributes;
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {ol.geom.Geometry} The geometry (or null if none).
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
goog.provide('ol.style.Line');
|
||||
goog.provide('ol.style.LiteralLine');
|
||||
|
||||
goog.require('ol.Expression');
|
||||
goog.require('ol.ExpressionLiteral');
|
||||
goog.require('ol.style.LiteralSymbolizer');
|
||||
goog.require('ol.style.Symbolizer');
|
||||
|
||||
|
||||
/**
|
||||
@@ -8,14 +12,14 @@ goog.require('ol.style.LiteralSymbolizer');
|
||||
* strokeWidth: (number),
|
||||
* opacity: (number)}}
|
||||
*/
|
||||
ol.style.LiteralLineConfig;
|
||||
ol.style.LiteralLineOptions;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements {ol.style.LiteralSymbolizer}
|
||||
* @param {ol.style.LiteralLineConfig} config Symbolizer properties.
|
||||
* @param {ol.style.LiteralLineOptions} config Symbolizer properties.
|
||||
*/
|
||||
ol.style.LiteralLine = function(config) {
|
||||
|
||||
@@ -29,3 +33,83 @@ ol.style.LiteralLine = function(config) {
|
||||
this.opacity = config.opacity;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{strokeStyle: (string|ol.Expression),
|
||||
* strokeWidth: (number|ol.Expression),
|
||||
* opacity: (number|ol.Expression)}}
|
||||
*/
|
||||
ol.style.LineOptions;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements {ol.style.Symbolizer}
|
||||
* @param {ol.style.LineOptions} options Symbolizer properties.
|
||||
*/
|
||||
ol.style.Line = function(options) {
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.strokeStyle_ = !goog.isDef(options.strokeStyle) ?
|
||||
new ol.ExpressionLiteral(ol.style.LineDefaults.strokeStyle) :
|
||||
(options.strokeStyle instanceof ol.Expression) ?
|
||||
options.strokeStyle : new ol.ExpressionLiteral(options.strokeStyle);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.strokeWidth_ = !goog.isDef(options.strokeWidth) ?
|
||||
new ol.ExpressionLiteral(ol.style.LineDefaults.strokeWidth) :
|
||||
(options.strokeWidth instanceof ol.Expression) ?
|
||||
options.strokeWidth : new ol.ExpressionLiteral(options.strokeWidth);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.opacity_ = !goog.isDef(options.opacity) ?
|
||||
new ol.ExpressionLiteral(ol.style.LineDefaults.opacity) :
|
||||
(options.opacity instanceof ol.Expression) ?
|
||||
options.opacity : new ol.ExpressionLiteral(options.opacity);
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return {ol.style.LiteralLine} Literal line symbolizer.
|
||||
*/
|
||||
ol.style.Line.prototype.createLiteral = function(feature) {
|
||||
var attrs = feature.getAttributes();
|
||||
|
||||
var strokeStyle = this.strokeStyle_.evaluate(feature, attrs);
|
||||
goog.asserts.assertString(strokeStyle, 'strokeStyle must be a string');
|
||||
|
||||
var strokeWidth = this.strokeWidth_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(strokeWidth, 'strokeWidth must be a number');
|
||||
|
||||
var opacity = this.opacity_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(opacity, 'opacity must be a number');
|
||||
|
||||
return new ol.style.LiteralLine({
|
||||
strokeStyle: strokeStyle,
|
||||
strokeWidth: strokeWidth,
|
||||
opacity: opacity
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @type {ol.style.LiteralLine}
|
||||
*/
|
||||
ol.style.LineDefaults = new ol.style.LiteralLine({
|
||||
strokeStyle: '#696969',
|
||||
strokeWidth: 1.5,
|
||||
opacity: 0.75
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
goog.provide('ol.style.LiteralPoint');
|
||||
goog.provide('ol.style.Point');
|
||||
|
||||
goog.require('ol.style.LiteralSymbolizer');
|
||||
goog.require('ol.style.Symbolizer');
|
||||
|
||||
|
||||
|
||||
@@ -9,3 +11,17 @@ goog.require('ol.style.LiteralSymbolizer');
|
||||
* @implements {ol.style.LiteralSymbolizer}
|
||||
*/
|
||||
ol.style.LiteralPoint = function() {};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements {ol.style.Symbolizer}
|
||||
*/
|
||||
ol.style.Point = function() {};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
ol.style.Point.prototype.createLiteral = goog.abstractMethod;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
goog.provide('ol.style.LiteralPolygon');
|
||||
goog.provide('ol.style.Polygon');
|
||||
|
||||
goog.require('ol.Expression');
|
||||
goog.require('ol.ExpressionLiteral');
|
||||
goog.require('ol.style.LiteralSymbolizer');
|
||||
goog.require('ol.style.Symbolizer');
|
||||
|
||||
|
||||
/**
|
||||
@@ -9,14 +13,14 @@ goog.require('ol.style.LiteralSymbolizer');
|
||||
* strokeWidth: (number),
|
||||
* opacity: (number)}}
|
||||
*/
|
||||
ol.style.LiteralPolygonConfig;
|
||||
ol.style.LiteralPolygonOptions;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements {ol.style.LiteralSymbolizer}
|
||||
* @param {ol.style.LiteralPolygonConfig} config Symbolizer properties.
|
||||
* @param {ol.style.LiteralPolygonOptions} config Symbolizer properties.
|
||||
*/
|
||||
ol.style.LiteralPolygon = function(config) {
|
||||
|
||||
@@ -33,3 +37,98 @@ ol.style.LiteralPolygon = function(config) {
|
||||
this.opacity = config.opacity;
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{fillStyle: (string|ol.Expression),
|
||||
* strokeStyle: (string|ol.Expression),
|
||||
* strokeWidth: (number|ol.Expression),
|
||||
* opacity: (number|ol.Expression)}}
|
||||
*/
|
||||
ol.style.PolygonOptions;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @implements {ol.style.Symbolizer}
|
||||
* @param {ol.style.PolygonOptions} options Symbolizer properties.
|
||||
*/
|
||||
ol.style.Polygon = function(options) {
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.fillStyle_ = !goog.isDef(options.fillStyle) ?
|
||||
new ol.ExpressionLiteral(ol.style.PolygonDefaults.fillStyle) :
|
||||
(options.fillStyle instanceof ol.Expression) ?
|
||||
options.fillStyle : new ol.ExpressionLiteral(options.fillStyle);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.strokeStyle_ = !goog.isDef(options.strokeStyle) ?
|
||||
new ol.ExpressionLiteral(ol.style.PolygonDefaults.strokeStyle) :
|
||||
(options.strokeStyle instanceof ol.Expression) ?
|
||||
options.strokeStyle : new ol.ExpressionLiteral(options.strokeStyle);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.strokeWidth_ = !goog.isDef(options.strokeWidth) ?
|
||||
new ol.ExpressionLiteral(ol.style.PolygonDefaults.strokeWidth) :
|
||||
(options.strokeWidth instanceof ol.Expression) ?
|
||||
options.strokeWidth : new ol.ExpressionLiteral(options.strokeWidth);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.opacity_ = !goog.isDef(options.opacity) ?
|
||||
new ol.ExpressionLiteral(ol.style.PolygonDefaults.opacity) :
|
||||
(options.opacity instanceof ol.Expression) ?
|
||||
options.opacity : new ol.ExpressionLiteral(options.opacity);
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return {ol.style.LiteralPolygon} Literal shape symbolizer.
|
||||
*/
|
||||
ol.style.Polygon.prototype.createLiteral = function(feature) {
|
||||
var attrs = feature.getAttributes();
|
||||
|
||||
var fillStyle = this.fillStyle_.evaluate(feature, attrs);
|
||||
goog.asserts.assertString(fillStyle, 'fillStyle must be a string');
|
||||
|
||||
var strokeStyle = this.strokeStyle_.evaluate(feature, attrs);
|
||||
goog.asserts.assertString(strokeStyle, 'strokeStyle must be a string');
|
||||
|
||||
var strokeWidth = this.strokeWidth_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(strokeWidth, 'strokeWidth must be a number');
|
||||
|
||||
var opacity = this.opacity_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(opacity, 'opacity must be a number');
|
||||
|
||||
return new ol.style.LiteralPolygon({
|
||||
fillStyle: fillStyle,
|
||||
strokeStyle: strokeStyle,
|
||||
strokeWidth: strokeWidth,
|
||||
opacity: opacity
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @type {ol.style.LiteralPolygon}
|
||||
*/
|
||||
ol.style.PolygonDefaults = new ol.style.LiteralPolygon({
|
||||
fillStyle: '#ffffff',
|
||||
strokeStyle: '#696969',
|
||||
strokeWidth: 1.5,
|
||||
opacity: 0.75
|
||||
});
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
goog.provide('ol.style.LiteralShape');
|
||||
goog.provide('ol.style.Shape');
|
||||
goog.provide('ol.style.ShapeType');
|
||||
|
||||
goog.require('ol.Expression');
|
||||
goog.require('ol.ExpressionLiteral');
|
||||
goog.require('ol.style.LiteralPoint');
|
||||
goog.require('ol.style.Point');
|
||||
|
||||
|
||||
/**
|
||||
@@ -20,14 +24,14 @@ ol.style.ShapeType = {
|
||||
* strokeWidth: (number),
|
||||
* opacity: (number)}}
|
||||
*/
|
||||
ol.style.LiteralShapeConfig;
|
||||
ol.style.LiteralShapeOptions;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @extends {ol.style.LiteralPoint}
|
||||
* @param {ol.style.LiteralShapeConfig} config Symbolizer properties.
|
||||
* @param {ol.style.LiteralShapeOptions} config Symbolizer properties.
|
||||
*/
|
||||
ol.style.LiteralShape = function(config) {
|
||||
|
||||
@@ -51,3 +55,123 @@ ol.style.LiteralShape = function(config) {
|
||||
|
||||
};
|
||||
goog.inherits(ol.style.LiteralShape, ol.style.LiteralPoint);
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{type: (ol.style.ShapeType),
|
||||
* size: (number|ol.Expression),
|
||||
* fillStyle: (string|ol.Expression),
|
||||
* strokeStyle: (string|ol.Expression),
|
||||
* strokeWidth: (number|ol.Expression),
|
||||
* opacity: (number|ol.Expression)}}
|
||||
*/
|
||||
ol.style.ShapeOptions;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
* @extends {ol.style.Point}
|
||||
* @param {ol.style.ShapeOptions} options Symbolizer properties.
|
||||
*/
|
||||
ol.style.Shape = function(options) {
|
||||
|
||||
/**
|
||||
* @type {ol.style.ShapeType}
|
||||
* @private
|
||||
*/
|
||||
this.type_ = /** @type {ol.style.ShapeType} */ goog.isDef(options.type) ?
|
||||
options.type : ol.style.ShapeDefaults.type;
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.size_ = !goog.isDef(options.size) ?
|
||||
new ol.ExpressionLiteral(ol.style.ShapeDefaults.size) :
|
||||
(options.size instanceof ol.Expression) ?
|
||||
options.size : new ol.ExpressionLiteral(options.size);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.fillStyle_ = !goog.isDef(options.fillStyle) ?
|
||||
new ol.ExpressionLiteral(ol.style.ShapeDefaults.fillStyle) :
|
||||
(options.fillStyle instanceof ol.Expression) ?
|
||||
options.fillStyle : new ol.ExpressionLiteral(options.fillStyle);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.strokeStyle_ = !goog.isDef(options.strokeStyle) ?
|
||||
new ol.ExpressionLiteral(ol.style.ShapeDefaults.strokeStyle) :
|
||||
(options.strokeStyle instanceof ol.Expression) ?
|
||||
options.strokeStyle : new ol.ExpressionLiteral(options.strokeStyle);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.strokeWidth_ = !goog.isDef(options.strokeWidth) ?
|
||||
new ol.ExpressionLiteral(ol.style.ShapeDefaults.strokeWidth) :
|
||||
(options.strokeWidth instanceof ol.Expression) ?
|
||||
options.strokeWidth : new ol.ExpressionLiteral(options.strokeWidth);
|
||||
|
||||
/**
|
||||
* @type {ol.Expression}
|
||||
* @private
|
||||
*/
|
||||
this.opacity_ = !goog.isDef(options.opacity) ?
|
||||
new ol.ExpressionLiteral(ol.style.ShapeDefaults.opacity) :
|
||||
(options.opacity instanceof ol.Expression) ?
|
||||
options.opacity : new ol.ExpressionLiteral(options.opacity);
|
||||
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @return {ol.style.LiteralShape} Literal shape symbolizer.
|
||||
*/
|
||||
ol.style.Shape.prototype.createLiteral = function(feature) {
|
||||
var attrs = feature.getAttributes();
|
||||
|
||||
var size = this.size_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(size, 'size must be a number');
|
||||
|
||||
var fillStyle = this.fillStyle_.evaluate(feature, attrs);
|
||||
goog.asserts.assertString(fillStyle, 'fillStyle must be a string');
|
||||
|
||||
var strokeStyle = this.strokeStyle_.evaluate(feature, attrs);
|
||||
goog.asserts.assertString(strokeStyle, 'strokeStyle must be a string');
|
||||
|
||||
var strokeWidth = this.strokeWidth_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(strokeWidth, 'strokeWidth must be a number');
|
||||
|
||||
var opacity = this.opacity_.evaluate(feature, attrs);
|
||||
goog.asserts.assertNumber(opacity, 'opacity must be a number');
|
||||
|
||||
return new ol.style.LiteralShape({
|
||||
type: this.type_,
|
||||
size: size,
|
||||
fillStyle: fillStyle,
|
||||
strokeStyle: strokeStyle,
|
||||
strokeWidth: strokeWidth,
|
||||
opacity: opacity
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @type {ol.style.LiteralShape}
|
||||
*/
|
||||
ol.style.ShapeDefaults = new ol.style.LiteralShape({
|
||||
type: ol.style.ShapeType.CIRCLE,
|
||||
size: 5,
|
||||
fillStyle: '#ffffff',
|
||||
strokeStyle: '#696969',
|
||||
strokeWidth: 1.5,
|
||||
opacity: 0.75
|
||||
});
|
||||
|
||||
51
test/spec/ol/style/line.test.js
Normal file
51
test/spec/ol/style/line.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
goog.provide('ol.test.style.Line');
|
||||
|
||||
describe('ol.style.Line', function() {
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('accepts literal values', function() {
|
||||
var symbolizer = new ol.style.Line({
|
||||
strokeStyle: '#BADA55',
|
||||
strokeWidth: 3
|
||||
});
|
||||
expect(symbolizer).toBeA(ol.style.Line);
|
||||
});
|
||||
|
||||
it('accepts expressions', function() {
|
||||
var symbolizer = new ol.style.Line({
|
||||
opacity: new ol.Expression('value / 100'),
|
||||
strokeWidth: ol.Expression('widthAttr')
|
||||
});
|
||||
expect(symbolizer).toBeA(ol.style.Line);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#createLiteral()', function() {
|
||||
|
||||
it('evaluates expressions with the given feature', function() {
|
||||
var symbolizer = new ol.style.Line({
|
||||
opacity: new ol.Expression('value / 100'),
|
||||
strokeWidth: ol.Expression('widthAttr')
|
||||
});
|
||||
|
||||
var feature = new ol.Feature(undefined, {
|
||||
value: 42,
|
||||
widthAttr: 1.5
|
||||
});
|
||||
|
||||
var literal = symbolizer.createLiteral(feature);
|
||||
expect(literal).toBeA(ol.style.LiteralLine);
|
||||
expect(literal.opacity).toBe(42 / 100);
|
||||
expect(literal.strokeWidth).toBe(1.5);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
goog.require('ol.Expression');
|
||||
goog.require('ol.Feature');
|
||||
goog.require('ol.style.Line');
|
||||
goog.require('ol.style.LiteralLine');
|
||||
51
test/spec/ol/style/polygon.test.js
Normal file
51
test/spec/ol/style/polygon.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
goog.provide('ol.test.style.Polygon');
|
||||
|
||||
describe('ol.style.Polygon', function() {
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('accepts literal values', function() {
|
||||
var symbolizer = new ol.style.Polygon({
|
||||
fillStyle: '#BADA55',
|
||||
strokeWidth: 3
|
||||
});
|
||||
expect(symbolizer).toBeA(ol.style.Polygon);
|
||||
});
|
||||
|
||||
it('accepts expressions', function() {
|
||||
var symbolizer = new ol.style.Polygon({
|
||||
opacity: new ol.Expression('value / 100'),
|
||||
fillStyle: ol.Expression('fillAttr')
|
||||
});
|
||||
expect(symbolizer).toBeA(ol.style.Polygon);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#createLiteral()', function() {
|
||||
|
||||
it('evaluates expressions with the given feature', function() {
|
||||
var symbolizer = new ol.style.Polygon({
|
||||
opacity: new ol.Expression('value / 100'),
|
||||
fillStyle: new ol.Expression('fillAttr')
|
||||
});
|
||||
|
||||
var feature = new ol.Feature(undefined, {
|
||||
value: 42,
|
||||
fillAttr: '#ff0000'
|
||||
});
|
||||
|
||||
var literal = symbolizer.createLiteral(feature);
|
||||
expect(literal).toBeA(ol.style.LiteralPolygon);
|
||||
expect(literal.opacity).toBe(42 / 100);
|
||||
expect(literal.fillStyle).toBe('#ff0000');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
goog.require('ol.Expression');
|
||||
goog.require('ol.Feature');
|
||||
goog.require('ol.style.Polygon');
|
||||
goog.require('ol.style.LiteralPolygon');
|
||||
51
test/spec/ol/style/shape.test.js
Normal file
51
test/spec/ol/style/shape.test.js
Normal file
@@ -0,0 +1,51 @@
|
||||
goog.provide('ol.test.style.Shape');
|
||||
|
||||
describe('ol.style.Shape', function() {
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('accepts literal values', function() {
|
||||
var symbolizer = new ol.style.Shape({
|
||||
size: 4,
|
||||
fillStyle: '#BADA55'
|
||||
});
|
||||
expect(symbolizer).toBeA(ol.style.Shape);
|
||||
});
|
||||
|
||||
it('accepts expressions', function() {
|
||||
var symbolizer = new ol.style.Shape({
|
||||
size: new ol.Expression('sizeAttr'),
|
||||
strokeStyle: ol.Expression('color')
|
||||
});
|
||||
expect(symbolizer).toBeA(ol.style.Shape);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#createLiteral()', function() {
|
||||
|
||||
it('evaluates expressions with the given feature', function() {
|
||||
var symbolizer = new ol.style.Shape({
|
||||
size: new ol.Expression('sizeAttr'),
|
||||
opacity: new ol.Expression('opacityAttr')
|
||||
});
|
||||
|
||||
var feature = new ol.Feature(undefined, {
|
||||
sizeAttr: 42,
|
||||
opacityAttr: 0.4
|
||||
});
|
||||
|
||||
var literal = symbolizer.createLiteral(feature);
|
||||
expect(literal).toBeA(ol.style.LiteralShape);
|
||||
expect(literal.size).toBe(42);
|
||||
expect(literal.opacity).toBe(0.4);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
goog.require('ol.Expression');
|
||||
goog.require('ol.Feature');
|
||||
goog.require('ol.style.Shape');
|
||||
goog.require('ol.style.LiteralShape');
|
||||
Reference in New Issue
Block a user