<commit_before>'use strict';

const defaultTo = require('lodash/defaultTo');
const VertexBuffer = require('../../webgl/vertex/VertexBuffer');
const WebGLInteractiveRenderer = require('./WebGLInteractiveRenderer');

// Constants

/**
 * Highlighted point radius increase.
 * @private
 * @constant {Number}
 */
const HIGHLIGHTED_RADIUS_OFFSET = 2;

/**
 * Selected point radius increase.
 * @private
 * @constant {Number}
 */
const SELECTED_RADIUS_OFFSET = 4;

/**
 * Shader GLSL source.
 * @private
 * @constant {Object}
 */
const SHADER_GLSL = {
	vert:
		`
		precision highp float;
		attribute vec2 aPosition;
		attribute float aRadius;
		uniform float uRadiusOffset;
		uniform vec2 uTileOffset;
		uniform float uScale;
		uniform float uPixelRatio;
		uniform mat4 uProjectionMatrix;
		void main() {
			vec2 wPosition = (aPosition * uScale) + uTileOffset;
			gl_PointSize = (aRadius + uRadiusOffset) * uScale * 2.0 * uPixelRatio;
			gl_Position = uProjectionMatrix * vec4(wPosition, 0.0, 1.0);
		}
		`,
	frag:
		`
		#ifdef GL_OES_standard_derivatives
			#extension GL_OES_standard_derivatives : enable
		#endif
		precision highp float;
		uniform vec4 uColor;
		void main() {
			vec2 cxy = 2.0 * gl_PointCoord - 1.0;
			float radius = dot(cxy, cxy);
			float alpha = 1.0;
			#ifdef GL_OES_standard_derivatives
				float delta = fwidth(radius);
				alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, radius);
			#else
				if (radius > 1.0) {
					discard;
				}
			#endif
			gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);
		}
		`
};

// Private Methods

const createPoint = function(gl) {
	const vertices = new Float32Array(2);
	vertices[0] = 0.0;
	vertices[1] = 0.0;
	// create quad buffer
	return new VertexBuffer(
		gl,
		vertices,
		{
			0: {
				size: 2,
				type: 'FLOAT'
			}
		},
		{
			mode: 'POINTS',
			count: 1
		});
};

const renderTiles = function(atlas, shader, renderables, color) {
	// set global uniforms
	shader.setUniform('uColor', color);
	shader.setUniform('uRadiusOffset', 0);

	// binds the buffer to instance
	atlas.bind();

	// for each renderable
	for (let i=0; i<renderables.length; i++) {
		const renderable = renderables[i];
		// set tile uniforms
		shader.setUniform('uScale', renderable.scale);
		shader.setUniform('uTileOffset', renderable.tileOffset);
		// draw points
		atlas.draw(renderable.hash, 'POINTS');
	}

	// unbind
	atlas.unbind();
};

const renderPoint = function(point, shader, plot, target, color, radius) {
	// get tile offset
	const coord = target.tile.coord;
	const scale = Math.pow(2, plot.zoom - coord.z);
	const viewport = plot.getViewportPixelOffset();
	const tileOffset = [
		(coord.x * scale * plot.tileSize) + (scale * target.x) - viewport.x,
		(coord.y * scale * plot.tileSize) + (scale * target.y) - viewport.y
	];
	// set uniforms
	shader.setUniform('uTileOffset', tileOffset);
	shader.setUniform('uScale', scale);
	shader.setUniform('uColor', color);
	shader.setUniform('uRadiusOffset', radius + target.radius);

	// binds the buffer to instance
	point.bind();

	// draw the points
	point.draw();

	// unbind
	point.unbind();
};

/**
 * Class representing an interactive point renderer.
 */
class InteractiveRenderer extends WebGLInteractiveRenderer {

	/**
	 * Instantiates a new InteractiveRenderer object.
	 *
	 * @param {Object} options - The options object.
	 * @param {Array} options.xField - The X field of the data.
	 * @param {Array} options.yField - The Y field of the data.
	 * @param {Array} options.radiusField - The radius field of the data.
	 * @param {Array} options.color - The color of the points.
	 */
	constructor(options = {}) {
		super(options);
		this.shader = null;
		this.point = null;
		this.atlas = null;
		this.xField = defaultTo(options.xField, 'x');
		this.yField = defaultTo(options.yField, 'y');
		this.radiusField = defaultTo(options.radiusField, 'radius');
		this.color = defaultTo(options.color, [ 1.0, 0.4, 0.1, 0.8 ]);
	}

	/**
	 * Executed when the layer is attached to a plot.
	 *
	 * @param {Layer} layer - The layer to attach the renderer to.
	 *
	 * @returns {Renderer} The renderer object, for chaining.
	 */
	onAdd(layer) {
		super.onAdd(layer);
		// get the extension for standard derivatives
		this.ext = this.gl.getExtension('OES_standard_derivatives');
		this.point = createPoint(this.gl);
		this.shader = this.createShader(SHADER_GLSL);
		this.atlas = this.createVertexAtlas({
			// position
			0: {
				size: 2,
				type: 'FLOAT'
			},
			// radius
			1: {
				size: 1,
				type: 'FLOAT'
			}
		});
		return this;
	}

	/**
	 * Executed when the layer is removed from a plot.
	 *
	 * @param {Layer} layer - The layer to remove the renderer from.
	 *
	 * @returns {Renderer} The renderer object, for chaining.
	 */
	onRemove(layer) {
		this.destroyVertexAtlas(this.atlas);
		this.atlas = null;
		this.shader = null;
		this.point = null;
		super.onRemove(layer);
		return this;
	}

	/**
	 * Executed when a tile is added to the layer pyramid.
	 *
	 * @param {VertexAtlas} atlas - The vertex atlas object.
	 * @param {Tile} tile - The new tile object containing data.
	 */
	addTile(atlas, tile) {
		const coord = tile.coord;
		const data = tile.data;
		const tileSize = this.layer.plot.tileSize;
		const xOffset = coord.x * tileSize;
		const yOffset = coord.y * tileSize;
		const xField = this.xField;
		const yField = this.yField;
		const radiusField = this.radiusField;
		const points = new Array(data.length);
		const vertices = new Float32Array(data.length * 3);
		for (let i=0; i<data.length; i++) {
			const datum = data[i];
			// get point attributes
			const x = datum[xField];
			const y = datum[yField];
			const radius = datum[radiusField];
			// convert to plot pixels
			const plotX = x + xOffset;
			const plotY = y + yOffset;
			// add to buffer
			vertices[i*3] = x;
			vertices[i*3+1] = y;
			vertices[i*3+2] = radius;
			// add to points
			points[i] = {
				x: x,
				y: y,
				radius: radius,
				minX: plotX - radius,
				maxX: plotX + radius,
				minY: plotY - radius,
				maxY: plotY + radius,
				tile: tile,
				data: datum
			};
		}
		// index points
		this.addPoints(coord, points);
		// add to atlas
		atlas.set(coord.hash, vertices, points.length);
	}

	/**
	 * Executed when a tile is removed from the layer pyramid.
	 *
	 * @param {VertexAtlas} atlas - The vertex atlas object.
	 * @param {Tile} tile - The new tile object containing data.
	 */
	removeTile(atlas, tile) {
		const coord = tile.coord;
		// remove from atlas
		atlas.delete(coord.hash);
		// unindex points
		this.removePoints(coord);
	}

	/**
	 * The draw function that is executed per frame.
	 *
	 * @param {Number} timestamp - The frame timestamp.
	 *
	 * @returns {Renderer} The renderer object, for chaining.
	 */
	draw() {
		const gl = this.gl;
		const layer = this.layer;
		const plot = layer.plot;
		const projection = this.getOrthoMatrix();
		const shader = this.shader;

		// bind render target
		plot.renderBuffer.bind();
		// clear render target
		plot.renderBuffer.clear();

		// set blending func
		gl.enable(gl.BLEND);
		gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

		// use shader
		shader.use();

		// set uniforms
		shader.setUniform('uProjectionMatrix', projection);
		shader.setUniform('uPixelRatio', plot.pixelRatio);

		// render the tiles
		renderTiles(
			this.atlas,
			shader,
			this.getRenderables(),
			this.color);

		// render selected
		layer.selected.forEach(selected => {
			renderPoint(
				this.point,
				shader,
				plot,
				selected,
				this.color,
				SELECTED_RADIUS_OFFSET);
		});

		// render highlighted
		if (layer.highlighted && !layer.isSelected(layer.highlighted)) {
			renderPoint(
				this.point,
				shader,
				plot,
				layer.highlighted,
				this.color,
				HIGHLIGHTED_RADIUS_OFFSET);
		}

		// unbind render target
		plot.renderBuffer.unbind();

		// render framebuffer to the backbuffer
		plot.renderBuffer.blitToScreen(this.layer.opacity);
		return this;
	}
}

module.exports = InteractiveRenderer;
<commit_msg>Use gettings in interactive renderer.<commit_after>'use strict';

const defaultTo = require('lodash/defaultTo');
const VertexBuffer = require('../../webgl/vertex/VertexBuffer');
const WebGLInteractiveRenderer = require('./WebGLInteractiveRenderer');

// Constants

/**
 * Highlighted point radius increase.
 * @private
 * @constant {Number}
 */
const HIGHLIGHTED_RADIUS_OFFSET = 2;

/**
 * Selected point radius increase.
 * @private
 * @constant {Number}
 */
const SELECTED_RADIUS_OFFSET = 4;

/**
 * Shader GLSL source.
 * @private
 * @constant {Object}
 */
const SHADER_GLSL = {
	vert:
		`
		precision highp float;
		attribute vec2 aPosition;
		attribute float aRadius;
		uniform float uRadiusOffset;
		uniform vec2 uTileOffset;
		uniform float uScale;
		uniform float uPixelRatio;
		uniform mat4 uProjectionMatrix;
		void main() {
			vec2 wPosition = (aPosition * uScale) + uTileOffset;
			gl_PointSize = (aRadius + uRadiusOffset) * uScale * 2.0 * uPixelRatio;
			gl_Position = uProjectionMatrix * vec4(wPosition, 0.0, 1.0);
		}
		`,
	frag:
		`
		#ifdef GL_OES_standard_derivatives
			#extension GL_OES_standard_derivatives : enable
		#endif
		precision highp float;
		uniform vec4 uColor;
		void main() {
			vec2 cxy = 2.0 * gl_PointCoord - 1.0;
			float radius = dot(cxy, cxy);
			float alpha = 1.0;
			#ifdef GL_OES_standard_derivatives
				float delta = fwidth(radius);
				alpha = 1.0 - smoothstep(1.0 - delta, 1.0 + delta, radius);
			#else
				if (radius > 1.0) {
					discard;
				}
			#endif
			gl_FragColor = vec4(uColor.rgb, uColor.a * alpha);
		}
		`
};

// Private Methods

const createPoint = function(gl) {
	const vertices = new Float32Array(2);
	vertices[0] = 0.0;
	vertices[1] = 0.0;
	// create quad buffer
	return new VertexBuffer(
		gl,
		vertices,
		{
			0: {
				size: 2,
				type: 'FLOAT'
			}
		},
		{
			mode: 'POINTS',
			count: 1
		});
};

const renderTiles = function(atlas, shader, renderables, color) {
	// set global uniforms
	shader.setUniform('uColor', color);
	shader.setUniform('uRadiusOffset', 0);

	// binds the buffer to instance
	atlas.bind();

	// for each renderable
	for (let i=0; i<renderables.length; i++) {
		const renderable = renderables[i];
		// set tile uniforms
		shader.setUniform('uScale', renderable.scale);
		shader.setUniform('uTileOffset', renderable.tileOffset);
		// draw points
		atlas.draw(renderable.hash, 'POINTS');
	}

	// unbind
	atlas.unbind();
};

const renderPoint = function(point, shader, plot, target, color, radius) {
	// get tile offset
	const coord = target.tile.coord;
	const scale = Math.pow(2, plot.zoom - coord.z);
	const viewport = plot.getViewportPixelOffset();
	const tileOffset = [
		(coord.x * scale * plot.tileSize) + (scale * target.x) - viewport.x,
		(coord.y * scale * plot.tileSize) + (scale * target.y) - viewport.y
	];
	// set uniforms
	shader.setUniform('uTileOffset', tileOffset);
	shader.setUniform('uScale', scale);
	shader.setUniform('uColor', color);
	shader.setUniform('uRadiusOffset', radius + target.radius);

	// binds the buffer to instance
	point.bind();

	// draw the points
	point.draw();

	// unbind
	point.unbind();
};

/**
 * Class representing an interactive point renderer.
 */
class InteractiveRenderer extends WebGLInteractiveRenderer {

	/**
	 * Instantiates a new InteractiveRenderer object.
	 *
	 * @param {Object} options - The options object.
	 * @param {Array} options.xField - The X field of the data.
	 * @param {Array} options.yField - The Y field of the data.
	 * @param {Array} options.radiusField - The radius field of the data.
	 * @param {Array} options.color - The color of the points.
	 */
	constructor(options = {}) {
		super(options);
		this.shader = null;
		this.point = null;
		this.atlas = null;
		this.xField = defaultTo(options.xField, 'x');
		this.yField = defaultTo(options.yField, 'y');
		this.radiusField = defaultTo(options.radiusField, 'radius');
		this.color = defaultTo(options.color, [ 1.0, 0.4, 0.1, 0.8 ]);
	}

	/**
	 * Executed when the layer is attached to a plot.
	 *
	 * @param {Layer} layer - The layer to attach the renderer to.
	 *
	 * @returns {Renderer} The renderer object, for chaining.
	 */
	onAdd(layer) {
		super.onAdd(layer);
		// get the extension for standard derivatives
		this.ext = this.gl.getExtension('OES_standard_derivatives');
		this.point = createPoint(this.gl);
		this.shader = this.createShader(SHADER_GLSL);
		this.atlas = this.createVertexAtlas({
			// position
			0: {
				size: 2,
				type: 'FLOAT'
			},
			// radius
			1: {
				size: 1,
				type: 'FLOAT'
			}
		});
		return this;
	}

	/**
	 * Executed when the layer is removed from a plot.
	 *
	 * @param {Layer} layer - The layer to remove the renderer from.
	 *
	 * @returns {Renderer} The renderer object, for chaining.
	 */
	onRemove(layer) {
		this.destroyVertexAtlas(this.atlas);
		this.atlas = null;
		this.shader = null;
		this.point = null;
		super.onRemove(layer);
		return this;
	}

	/**
	 * Executed when a tile is added to the layer pyramid.
	 *
	 * @param {VertexAtlas} atlas - The vertex atlas object.
	 * @param {Tile} tile - The new tile object containing data.
	 */
	addTile(atlas, tile) {
		const coord = tile.coord;
		const data = tile.data;
		const tileSize = this.layer.plot.tileSize;
		const xOffset = coord.x * tileSize;
		const yOffset = coord.y * tileSize;
		const xField = this.xField;
		const yField = this.yField;
		const radiusField = this.radiusField;
		const points = new Array(data.length);
		const vertices = new Float32Array(data.length * 3);
		for (let i=0; i<data.length; i++) {
			const datum = data[i];
			// get point attributes
			const x = datum[xField];
			const y = datum[yField];
			const radius = datum[radiusField];
			// convert to plot pixels
			const plotX = x + xOffset;
			const plotY = y + yOffset;
			// add to buffer
			vertices[i*3] = x;
			vertices[i*3+1] = y;
			vertices[i*3+2] = radius;
			// add to points
			points[i] = {
				x: x,
				y: y,
				radius: radius,
				minX: plotX - radius,
				maxX: plotX + radius,
				minY: plotY - radius,
				maxY: plotY + radius,
				tile: tile,
				data: datum
			};
		}
		// index points
		this.addPoints(coord, points);
		// add to atlas
		atlas.set(coord.hash, vertices, points.length);
	}

	/**
	 * Executed when a tile is removed from the layer pyramid.
	 *
	 * @param {VertexAtlas} atlas - The vertex atlas object.
	 * @param {Tile} tile - The new tile object containing data.
	 */
	removeTile(atlas, tile) {
		const coord = tile.coord;
		// remove from atlas
		atlas.delete(coord.hash);
		// unindex points
		this.removePoints(coord);
	}

	/**
	 * The draw function that is executed per frame.
	 *
	 * @param {Number} timestamp - The frame timestamp.
	 *
	 * @returns {Renderer} The renderer object, for chaining.
	 */
	draw() {
		const gl = this.gl;
		const layer = this.layer;
		const plot = layer.plot;
		const projection = this.getOrthoMatrix();
		const shader = this.shader;

		// bind render target
		plot.renderBuffer.bind();
		// clear render target
		plot.renderBuffer.clear();

		// set blending func
		gl.enable(gl.BLEND);
		gl.blendFunc(gl.SRC_ALPHA, gl.ONE);

		// use shader
		shader.use();

		// set uniforms
		shader.setUniform('uProjectionMatrix', projection);
		shader.setUniform('uPixelRatio', plot.pixelRatio);

		// render the tiles
		renderTiles(
			this.atlas,
			shader,
			this.getRenderables(),
			this.color);

		// render selected
		layer.getSelected().forEach(selected => {
			renderPoint(
				this.point,
				shader,
				plot,
				selected,
				this.color,
				SELECTED_RADIUS_OFFSET);
		});

		// render highlighted
		const highlighted = layer.getHighlighted();
		if (highlighted && !layer.isSelected(highlighted)) {
			renderPoint(
				this.point,
				shader,
				plot,
				highlighted,
				this.color,
				HIGHLIGHTED_RADIUS_OFFSET);
		}

		// unbind render target
		plot.renderBuffer.unbind();

		// render framebuffer to the backbuffer
		plot.renderBuffer.blitToScreen(this.layer.opacity);
		return this;
	}
}

module.exports = InteractiveRenderer;
<|endoftext|>


<commit_before>/**
 * Field is the base for actual field type elements.
 */
export class Field {
  /**
   * Match a form field reference optionally followed by a JS field or function.
   *
   * Examples:
   *   {@linkplain #/reference/to/form/field:jsFieldName}
   *   {@linkplain #/another/reference:jsFunctionName()}
   *   {@linkplain #/third/reference/without/js/field}
  */
  static MATCH_REFERENCE_PLUS_FIELD = /\${(.+?)(\:([a-zA-Z0-9]+(\(\))?))?}/g
  /**
   * Match the string {@linkplain $index}
   * @type {String}
   */
  static MATCH_INDEX = /\$index/g
  /**
   * The ID of the field. Not displayed to the user directly.
   * @type {String}
   */
  id = '';
  /**
   * The display and/or output format of the field. The field implementation may
   * or may not ignore this.
   * @type {String}
   */
  format = '';
  /**
   * The number of columns this element should use.
   * @type {Number}
   */
  columns = 8;
  /**
   * The conditions on which to display this field.
   * @type {String}
   */
  conditions = {};
  /**
   * The index of this element within the parent. This should only be defined if
   * the parent stores children using numerical indexes. For object-like child
   * storage, the {@link #id} field should be used instead.
   * @type {Number}
   */
  index = undefined;
  /**
   * The parent of this field.
   * @type {Field}
   */
  parent = undefined;
  /**
   * Whether or not the parent should include the value of this field in its
   * value. Useful to set to false when using {@link Linkfield}s
   * @type {Boolean}
   */
  showValueInParent = true
  /**
   * Whether or not the value of this field should be hidden from the output
   * when it's empty.
   * @type {Boolean}
   */
  hideValueIfEmpty = false
  /**
   * Whether or not this field has the field {@link #collapsed} and the method
   * {@link #toggleCollapse()}
   *
   * @type {Boolean}
   */
  isCollapsible = false
  /**
   * Functions that will be called when this field changes.
   * @type {Function[]}
   */
  changeListeners = []
  /**
   * The path to use as the I18n path of this field instead of the default.
   * @type {String}
   */
  overrideI18nPath = undefined
  /**
   * Object that contains cached localizations for this field for the current
   * language. Use {@link #i18n()} instead of directly accessing this object.
   * This object is cleared when the locale changes so that the localizations
   * would be updated.
   * @type {Object}
   */
  localizations = {}

  get i18nPath() {
    if (this.overrideI18nPath) {
      return this.overrideI18nPath;
    } else if (!this.cachedI18nPath) {
      if (!this.parent) {
        this.cachedI18nPath = this.id;
      } else if (this.parent.type === 'array') {
        this.cachedI18nPath = `${this.parent.i18nPath}.item`;
      } else {
        this.cachedI18nPath = `${this.parent.i18nPath}.${this.id}`;
      }
    }
    return this.cachedI18nPath;
  }

  i18n(fieldName, defaultValue) {
    if (!fieldName) {
      fieldName = 'label';
    }
    if (!this.localizations.hasOwnProperty(fieldName)) {
      const path = `${this.i18nPath}.${fieldName}`;
      let translation = Field.internationalizer.tr(path);
      if (!translation || (typeof defaultValue == 'string' && translation === path)) {
        translation = defaultValue;
      }
      this.localizations[fieldName] = translation;
      return translation;
    }
    return this.localizations[fieldName];
  }

  /**
   * Initialize this field with the base data.
   * @param  {String} id                The index of this field.
   * @param  {Number} [args.columns=8]  The number of columns this field should
   *                                    use.
   * @param  {String} [args.format]     The output and/or display format of the
   *                                    field.
   * @param  {Field}  [args.parent]     The parent of this field.
   * @param  {Number} [args.index]      The numerical index of this field within
   *                                    the parent.
   * @param  {Object} [args.conditions] The display conditions of this field.
   * @param  {Boolean} [args.showValueInParent] Whether or not the parent should
   *                                            include the value of this field
   *                                            in its value.
   * @param  {Boolean} [args.hideValueIfEmpty]  Whether or not the value of this
   *                                            field should be hidden from the
   *                                            output when its empty.
   * @param {String}   [args.overrideI18nPath]  The path to use as the I18n path
   *                                            of this field instead of the
   *                                            default.
   * @return {Field}                    This field.
   */
  init(id, args = {}) {
    args = Object.assign({
      columns: 8,
      format: '',
      conditions: {},
      showValueInParent: true,
      hideValueIfEmpty: false,
      overrideI18nPath: undefined
    }, args);
    Field.eventAggregator.subscribe('i18n:locale:changed', () => this.localizations = {});
    this.id = id;
    this.format = args.format;
    this.conditions = args.conditions;
    this.columns = args.columns;
    this.index = args.index;
    this.parent = args.parent;
    this.showValueInParent = args.showValueInParent;
    this.hideValueIfEmpty = args.hideValueIfEmpty;
    this.overrideI18nPath = args.overrideI18nPath;
    this.type = this.constructor.TYPE;
    return this;
  }

  /**
   * Check if this field is empty. By default, this returns false, but all field
   * implementations should override this.
   */
  isEmpty() {
    return true;
  }
  /**
   * Recursively get an unique identifier for this field.
   */
  get path() {
    if (!this.parent) {
      return this.id;
    }
    return `${this.parent.path}.${this.id}`;
  }

  get display() {
    return this.shouldDisplay();
  }

  shouldDisplay() {
    for (const [path, value] of Object.entries(this.conditions)) {
      const elem = this.resolveRef(path);

      if (!elem) {
        return false;
      }

      const elemValue = elem.getValue();

      if (Array.isArray(value)) {
        if (!value.includes(elemValue)) {
          return false;
        }
      } else if (value !== elemValue) {
        return false;
      }
    }
    return true;
  }

  /**
   * The displayed label for the field. This might be a header (for objects) or
   * just a label (for inputs).
   * @return {String} The label to display.
   */
  get label() {
    let label = this.i18n('label');
    if (!label.includes('$')) {
      return label;
    }

    return this.formatReferencePlusField(this.formatIndex(label));
  }

  get helpText() {
    return this.i18n('helpText', '');
  }

  /**
   * Replace each instance of {@linkplain $index} with the index of this field.
   * @param  {String} string The string to replace the occurences in.
   * @return {String}        The string with all the occurences replaced.
   */
  formatIndex(string) {
    return string.replace(Field.MATCH_INDEX, this.index + 1);
  }

  /**
   * Replace all field references with the values of the references.
   * See {@link #MATCH_REFERENCE_PLUS_FIELD} to see what kind of references are allowed.
   * @param  {String} string The string to replace the occurences in.
   * @return {String}        The string with all the occurences replaced.
   */
  formatReferencePlusField(string) {
    return string.replace(Field.MATCH_REFERENCE_PLUS_FIELD, (match, path, _, field) => {
      const elem = this.resolveRef(path);
      if (elem !== undefined) {
        if (field !== undefined) {
          return elem.getFieldValue(field);
        }
        // Field name not specified, return the value of the form field.
        return elem.getValue();
      }
      // Form field not found.
      return '';
    });
  }

  /**
   * Get the value of the given field or function.
   * @param  {String} name The name of the field. If the field is a function that
   *                       should be called, add {@linkplain ()} to the end.
   * @return {[type]}      [description]
   */
  getFieldValue(name) {
    if (name === undefined) {
      return undefined;
    }

    if (name.endsWith('()')) {
      // Field name specified with braces, so call the field as a function.
      return this[name.substr(0, name.length - 2)]();
    }
    // Field name specified without braces, so just get the value of that field.
    return this[name];
  }

  /**
   * Resolve a path (in JSON reference format) relative to this field.
   *
   * @param  {String} ref The JSON reference to resolve.
   * @return {Field}      The field at the path, or undefined if not found.
   */
  resolveRef(ref) {
    return this.resolvePath(ref.split('/'));
  }

  /**
   * Resolve a path (array) relative to this field.
   *
   * @param  {String[]} path The path to resolve.
   * @return {Field}         The field at the path, or undefined if not found.
   */
  resolvePath(path) {
    if (path.length === 0) {
      return this;
    } else if (path[0] === '.' || path[0] === '#') {
      return this.resolvePath(path.splice(1));
    } else if (path[0] === '..') {
      return this.parent.resolvePath(path.splice(1));
    } else if (path[0].length === 0) {
      return this.superParent().resolvePath(path.splice(1));
    }
    return undefined;
  }

  /**
   * Find the top-level parent of this field.
   *
   * @return {Field} The top-level parent field.
   */
  superParent() {
    if (this.parent) {
      return this.parent.superParent();
    }
    return this;
  }

  /**
   * Delete this field from the parent.
   */
  delete() {
    if (this.parent) {
      this.parent.deleteChild(typeof this.index === 'number' ? this.index : this.id);
    }
  }

  /**
   * Get the value of this field.
   *
   * @return {Object} The value of this field.
   */
  getValue() {
    return undefined;
  }

  /**
   * Set the value of this field.
   *
   * @param {Object} value The new value to set to this field.
   */
  setValue(value) { }

  onChange(field) {
    field = field || this;
    if (this.parent) {
      this.parent.onChange(field);
    }

    for (const listener of this.changeListeners) {
      listener(field);
    }
  }

  addChangeListener(func) {
    this.changeListeners.push(func);
  }

  /**
   * Clone this field.
   *
   * @param {Field} parent The new parent of this field.
   * @return {Field} A deep clone of this field.
   */
  clone(parent) {
    const ExtendedClass = Object.getPrototypeOf(this).constructor;
    const clone = new ExtendedClass();
    clone.init(this.id, this);
    if (parent) {
      clone.parent = parent;
    }
    return clone;
  }

  // Functions implemented in Collapsiblefield.
  setCollapsed() {}
  toggleCollapse() {}
  childCollapseChanged() {}
}
<commit_msg>Fix defaultValue type checking<commit_after>/**
 * Field is the base for actual field type elements.
 */
export class Field {
  /**
   * Match a form field reference optionally followed by a JS field or function.
   *
   * Examples:
   *   {@linkplain #/reference/to/form/field:jsFieldName}
   *   {@linkplain #/another/reference:jsFunctionName()}
   *   {@linkplain #/third/reference/without/js/field}
  */
  static MATCH_REFERENCE_PLUS_FIELD = /\${(.+?)(\:([a-zA-Z0-9]+(\(\))?))?}/g
  /**
   * Match the string {@linkplain $index}
   * @type {String}
   */
  static MATCH_INDEX = /\$index/g
  /**
   * The ID of the field. Not displayed to the user directly.
   * @type {String}
   */
  id = '';
  /**
   * The display and/or output format of the field. The field implementation may
   * or may not ignore this.
   * @type {String}
   */
  format = '';
  /**
   * The number of columns this element should use.
   * @type {Number}
   */
  columns = 8;
  /**
   * The conditions on which to display this field.
   * @type {String}
   */
  conditions = {};
  /**
   * The index of this element within the parent. This should only be defined if
   * the parent stores children using numerical indexes. For object-like child
   * storage, the {@link #id} field should be used instead.
   * @type {Number}
   */
  index = undefined;
  /**
   * The parent of this field.
   * @type {Field}
   */
  parent = undefined;
  /**
   * Whether or not the parent should include the value of this field in its
   * value. Useful to set to false when using {@link Linkfield}s
   * @type {Boolean}
   */
  showValueInParent = true
  /**
   * Whether or not the value of this field should be hidden from the output
   * when it's empty.
   * @type {Boolean}
   */
  hideValueIfEmpty = false
  /**
   * Whether or not this field has the field {@link #collapsed} and the method
   * {@link #toggleCollapse()}
   *
   * @type {Boolean}
   */
  isCollapsible = false
  /**
   * Functions that will be called when this field changes.
   * @type {Function[]}
   */
  changeListeners = []
  /**
   * The path to use as the I18n path of this field instead of the default.
   * @type {String}
   */
  overrideI18nPath = undefined
  /**
   * Object that contains cached localizations for this field for the current
   * language. Use {@link #i18n()} instead of directly accessing this object.
   * This object is cleared when the locale changes so that the localizations
   * would be updated.
   * @type {Object}
   */
  localizations = {}

  get i18nPath() {
    if (this.overrideI18nPath) {
      return this.overrideI18nPath;
    } else if (!this.cachedI18nPath) {
      if (!this.parent) {
        this.cachedI18nPath = this.id;
      } else if (this.parent.type === 'array') {
        this.cachedI18nPath = `${this.parent.i18nPath}.item`;
      } else {
        this.cachedI18nPath = `${this.parent.i18nPath}.${this.id}`;
      }
    }
    return this.cachedI18nPath;
  }

  i18n(fieldName, defaultValue) {
    if (!fieldName) {
      fieldName = 'label';
    }
    if (!this.localizations.hasOwnProperty(fieldName)) {
      const path = `${this.i18nPath}.${fieldName}`;
      let translation = Field.internationalizer.tr(path);
      if (!translation || (typeof defaultValue === 'string' && translation === path)) {
        translation = defaultValue;
      }
      this.localizations[fieldName] = translation;
      return translation;
    }
    return this.localizations[fieldName];
  }

  /**
   * Initialize this field with the base data.
   * @param  {String} id                The index of this field.
   * @param  {Number} [args.columns=8]  The number of columns this field should
   *                                    use.
   * @param  {String} [args.format]     The output and/or display format of the
   *                                    field.
   * @param  {Field}  [args.parent]     The parent of this field.
   * @param  {Number} [args.index]      The numerical index of this field within
   *                                    the parent.
   * @param  {Object} [args.conditions] The display conditions of this field.
   * @param  {Boolean} [args.showValueInParent] Whether or not the parent should
   *                                            include the value of this field
   *                                            in its value.
   * @param  {Boolean} [args.hideValueIfEmpty]  Whether or not the value of this
   *                                            field should be hidden from the
   *                                            output when its empty.
   * @param {String}   [args.overrideI18nPath]  The path to use as the I18n path
   *                                            of this field instead of the
   *                                            default.
   * @return {Field}                    This field.
   */
  init(id, args = {}) {
    args = Object.assign({
      columns: 8,
      format: '',
      conditions: {},
      showValueInParent: true,
      hideValueIfEmpty: false,
      overrideI18nPath: undefined
    }, args);
    Field.eventAggregator.subscribe('i18n:locale:changed', () => this.localizations = {});
    this.id = id;
    this.format = args.format;
    this.conditions = args.conditions;
    this.columns = args.columns;
    this.index = args.index;
    this.parent = args.parent;
    this.showValueInParent = args.showValueInParent;
    this.hideValueIfEmpty = args.hideValueIfEmpty;
    this.overrideI18nPath = args.overrideI18nPath;
    this.type = this.constructor.TYPE;
    return this;
  }

  /**
   * Check if this field is empty. By default, this returns false, but all field
   * implementations should override this.
   */
  isEmpty() {
    return true;
  }
  /**
   * Recursively get an unique identifier for this field.
   */
  get path() {
    if (!this.parent) {
      return this.id;
    }
    return `${this.parent.path}.${this.id}`;
  }

  get display() {
    return this.shouldDisplay();
  }

  shouldDisplay() {
    for (const [path, value] of Object.entries(this.conditions)) {
      const elem = this.resolveRef(path);

      if (!elem) {
        return false;
      }

      const elemValue = elem.getValue();

      if (Array.isArray(value)) {
        if (!value.includes(elemValue)) {
          return false;
        }
      } else if (value !== elemValue) {
        return false;
      }
    }
    return true;
  }

  /**
   * The displayed label for the field. This might be a header (for objects) or
   * just a label (for inputs).
   * @return {String} The label to display.
   */
  get label() {
    let label = this.i18n('label');
    if (!label.includes('$')) {
      return label;
    }

    return this.formatReferencePlusField(this.formatIndex(label));
  }

  get helpText() {
    return this.i18n('helpText', '');
  }

  /**
   * Replace each instance of {@linkplain $index} with the index of this field.
   * @param  {String} string The string to replace the occurences in.
   * @return {String}        The string with all the occurences replaced.
   */
  formatIndex(string) {
    return string.replace(Field.MATCH_INDEX, this.index + 1);
  }

  /**
   * Replace all field references with the values of the references.
   * See {@link #MATCH_REFERENCE_PLUS_FIELD} to see what kind of references are allowed.
   * @param  {String} string The string to replace the occurences in.
   * @return {String}        The string with all the occurences replaced.
   */
  formatReferencePlusField(string) {
    return string.replace(Field.MATCH_REFERENCE_PLUS_FIELD, (match, path, _, field) => {
      const elem = this.resolveRef(path);
      if (elem !== undefined) {
        if (field !== undefined) {
          return elem.getFieldValue(field);
        }
        // Field name not specified, return the value of the form field.
        return elem.getValue();
      }
      // Form field not found.
      return '';
    });
  }

  /**
   * Get the value of the given field or function.
   * @param  {String} name The name of the field. If the field is a function that
   *                       should be called, add {@linkplain ()} to the end.
   * @return {[type]}      [description]
   */
  getFieldValue(name) {
    if (name === undefined) {
      return undefined;
    }

    if (name.endsWith('()')) {
      // Field name specified with braces, so call the field as a function.
      return this[name.substr(0, name.length - 2)]();
    }
    // Field name specified without braces, so just get the value of that field.
    return this[name];
  }

  /**
   * Resolve a path (in JSON reference format) relative to this field.
   *
   * @param  {String} ref The JSON reference to resolve.
   * @return {Field}      The field at the path, or undefined if not found.
   */
  resolveRef(ref) {
    return this.resolvePath(ref.split('/'));
  }

  /**
   * Resolve a path (array) relative to this field.
   *
   * @param  {String[]} path The path to resolve.
   * @return {Field}         The field at the path, or undefined if not found.
   */
  resolvePath(path) {
    if (path.length === 0) {
      return this;
    } else if (path[0] === '.' || path[0] === '#') {
      return this.resolvePath(path.splice(1));
    } else if (path[0] === '..') {
      return this.parent.resolvePath(path.splice(1));
    } else if (path[0].length === 0) {
      return this.superParent().resolvePath(path.splice(1));
    }
    return undefined;
  }

  /**
   * Find the top-level parent of this field.
   *
   * @return {Field} The top-level parent field.
   */
  superParent() {
    if (this.parent) {
      return this.parent.superParent();
    }
    return this;
  }

  /**
   * Delete this field from the parent.
   */
  delete() {
    if (this.parent) {
      this.parent.deleteChild(typeof this.index === 'number' ? this.index : this.id);
    }
  }

  /**
   * Get the value of this field.
   *
   * @return {Object} The value of this field.
   */
  getValue() {
    return undefined;
  }

  /**
   * Set the value of this field.
   *
   * @param {Object} value The new value to set to this field.
   */
  setValue(value) { }

  onChange(field) {
    field = field || this;
    if (this.parent) {
      this.parent.onChange(field);
    }

    for (const listener of this.changeListeners) {
      listener(field);
    }
  }

  addChangeListener(func) {
    this.changeListeners.push(func);
  }

  /**
   * Clone this field.
   *
   * @param {Field} parent The new parent of this field.
   * @return {Field} A deep clone of this field.
   */
  clone(parent) {
    const ExtendedClass = Object.getPrototypeOf(this).constructor;
    const clone = new ExtendedClass();
    clone.init(this.id, this);
    if (parent) {
      clone.parent = parent;
    }
    return clone;
  }

  // Functions implemented in Collapsiblefield.
  setCollapsed() {}
  toggleCollapse() {}
  childCollapseChanged() {}
}
<|endoftext|>


<commit_before>'use strict';

(function(app) {

  /**
   * Defines the video controller for the videos page.
   */
  function VideoController(
  $scope,
  $filter,
  $location,
  $window,
  $interval,
  $timeout,
  entityService,
  publishService,
  properties,
  categories,
  platforms,
  jsonPath,
  tableReloadEventService,
  i18nService) {

    $scope.properties = properties.data.entities;

    // Replace Id in Video by the name of the category
    // Category Id can be overwritten, it is only for display purpose
    $scope.categories = categories.data.taxonomy;
    $scope.platforms = platforms.data.platforms;

    /*
     *
     * RIGHTS
     *
     */
    $scope.rights = {};
    $scope.rights.edit = $scope.checkAccess('update-video');
    $scope.rights.delete = $scope.checkAccess('delete-video');
    $scope.rights.publish = $scope.checkAccess('publish-video');
    $scope.rights.chapter = $scope.checkAccess('chapter-video');

    /*
     * FORM EDIT
     */
    var scopeEditForm = $scope.editFormContainer = {};
    scopeEditForm.model = {};
    scopeEditForm.pendingEdition = false;

    /*
     * DATATABLE
     */
    var scopeDataTable = $scope.tableContainer = {};

    /**
     * Gets all categories and add a value for "none".
     * @param {String} label The label for "no categories"
     * @return {Array} The list of categories
     */
    function getSelectableCategories(label) {
      return [{
        value: null,
        name: $filter('translate')(label)
      }].concat(publishService.getCategoriesOptions());
    }

    /**
     * Opens a link in a new tab.
     * @param {String} link Destination url
     */
    function goToPath(link) {
      $window.open(link, '_blank');
    }

    /**
     * Opens an alert to display HTML code to share the video.
     * @param {Object} video The video to share
     */
    function shareCode(video) {
      $scope.$emit('setAlert', 'info', [
        $filter('translate')('VIDEOS.SHARECODE'),
        '

',
        '<iframe width="480" height="270" ',
        'src="' + $location.protocol() + '://' + $location.host() + ':' + $location.port() + video.link,
        '?fullscreen&lang=' + i18nService.getLanguage() + '"',
        ' frameborder="0"></iframe>',
        '
'
      ].join(''), 0);
    }

    // Iterate through the list of videos, if at least one video
    // is pending, poll each 30 seconds to be informed of
    // its status
    var pollVideosPromise = $interval(function() {
      if (!scopeEditForm.pendingEdition) {
        $scope.$emit('setAlert', 'info', $filter('translate')('VIDEOS.RELOAD'), 4000);
        entityService.deleteCache(scopeDataTable.entityType);
        tableReloadEventService.broadcast();
      }
    }, 30000);

    /**
     * Retries a video which is on error.
     * @param {Array} videos The list of videos to work on
     * @param {Function} reload Function to reload the datatable
     */
    function retryVideo(videos, reload) {
      publishService.retryVideo(videos.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.RETRY_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Asks server to start uploading the video to the chosen platform.
     * @param {Array} videos The list of videos to work on
     * @param {String} platformName The name of platform to upload to
     * @param {Function} reload Function to reload the datatable
     */
    function startVideoUpload(videos, platformName, reload) {
      publishService.startVideoUpload(videos.join(','), platformName)
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.UPLOAD_START_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Publishes a list of videos.
     * @param {Array} videos The list of video ids to publish
     * @param {Function} reload Function to reload the datatable
     */
    function publishVideo(videos, reload) {
      publishService.publishVideo(videos.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.PUBLISHED_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Unpublishes a list of videos.
     * @param {Array} videos The list of video ids to unpublish
     * @param {Function} reload Function to reload the datatable
     */
    function unpublishVideo(videos, reload) {
      publishService.unpublishVideo(videos.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.UNPUBLISHED_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Removes a list of videos.
     * @param {Array} selected The list of video ids to remove
     * @param {Function} reload Function to reload the datatable
     */
    function removeRows(selected, reload) {
      entityService.removeEntity('video', selected.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.REMOVE_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Saves video information.
     * @param {Object} video Video data
     */
    function saveVideo(video) {
      return entityService.updateEntity('video', video.id, {
        title: video.title,
        description: video.description,
        properties: video.properties,
        category: video.category
      }).then(function() {
        scopeEditForm.pendingEdition = false;
      });
    }

    /**
     * Routes to chapters edition.
     * @param {Object} video The video to edit
     */
    function editChapter(video) {
      $location.path('/publish/video/' + video.id);
    }

    /**
     * Gets property name by id.
     * @param {String} id Id of the property
     * @return {String} The property name
     */
    function replacePropIdByName(id) {
      var found = -1;
      for (var i = 0; i < $scope.properties.length && found < 0; i++) {
        var value = $scope.properties[i];
        if (value.id == String(id)) {
          found = i;
          break;
        }
      }

      if (found != -1)
        return $scope.properties[found].name;
      else
        return id;
    }

    /*
     * FORM EDIT
     */
    scopeEditForm.entityType = 'video';

    var categoriesfield = getSelectableCategories('UI.NONE');
    scopeEditForm.fieldsBase = [
      {
        key: 'title',
        type: 'horizontalExtendInput',
        templateOptions: {
          label: $filter('translate')('VIDEOS.ATTR_TITLE'),
          required: true
        }
      },
      {
        key: 'description',
        type: 'horizontalExtendInput',
        templateOptions: {
          label: $filter('translate')('VIDEOS.ATTR_DESCRIPTION'),
          required: true
        }
      },
      {
        key: 'category',
        type: 'horizontalExtendSelect',
        templateOptions: {
          label: $filter('translate')('VIDEOS.ATTR_CATEGORY'),
          options: categoriesfield
        }
      }
    ];

    /*
     *
     * DATATABLE
     */
    scopeDataTable.entityType = 'video';
    scopeDataTable.cellTheme = '/publish/be/views/partial/publishCells.html';
    scopeDataTable.conditionTogleDetail = function(row) {
      return (row.state === 11 || row.state === 12);
    };

    scopeDataTable.init = {
      sortBy: 'date',
      sortOrder: 'dsc'
    };
    var categoriesFilter = getSelectableCategories('UI.ALL');
    scopeDataTable.filterBy = [
      {
        key: 'title',
        value: '',
        label: $filter('translate')('VIDEOS.TITLE_FILTER')
      }, {
        key: 'description',
        value: '',
        label: $filter('translate')('VIDEOS.DESCRIPTION_FILTER')
      }, {
        key: 'date',
        type: 'date',
        value: '',
        label: $filter('translate')('VIDEOS.DATE_FILTER')
      }, {
        key: 'category',
        type: 'select',
        value: null,
        label: $filter('translate')('VIDEOS.CATEGORY_FILTER'),

        /*
         * [{
         "value": 'id',
         "name": 'title',
         "children" : 'id1,id2,id3'
         },
         ...
         ];
         */
        options: categoriesFilter,

        // if enable filter will filter with the selectId AND additionnal id set in the "children" key of each options
        filterWithChildren: true
      }
    ];
    scopeDataTable.header = [
      {
        key: 'title',
        name: $filter('translate')('VIDEOS.NAME_COLUMN'),
        class: ['col-xs-12 col-sm-5']
      },
      {
        key: 'date',
        type: 'date',
        name: $filter('translate')('VIDEOS.DATE_COLUMN'),
        class: ['hidden-xs col-sm-1']
      },
      {
        key: 'category',
        type: 'category',
        name: $filter('translate')('VIDEOS.CATEGORY_COLUMN'),
        class: ['hidden-xs col-sm-3']
      },
      {
        key: 'state',
        type: 'status',
        name: $filter('translate')('VIDEOS.STATUS_COLUMN'),
        class: ['hidden-xs col-sm-2']
      },
      {
        key: 'action',
        name: $filter('translate')('UI.ACTIONS_COLUMN'),
        class: [' hidden-xs col-sm-1']
      }];
    scopeDataTable.actions = [
      {
        label: $filter('translate')('UI.VIEW'),
        condition: function(row) {
          return row.state == 12;
        },
        callback: function(row) {
          goToPath(row.link);
        }
      },
      {
        label: $filter('translate')('UI.SHARE'),
        condition: function(row) {
          return row.state == 12;
        },
        callback: function(row) {
          shareCode(row);
        }
      },
      {
        label: $filter('translate')('VIDEOS.PUBLISH'),
        condition: function(row) {
          return $scope.rights.publish && row.state == 11 && !row.saving;
        },
        callback: function(row, reload) {
          publishVideo([row.id], reload);
        },
        global: function(selected, reload) {
          publishVideo(selected, reload);
        }
      },
      {
        label: $filter('translate')('VIDEOS.UNPUBLISH'),
        condition: function(row) {
          return $scope.rights.publish && row.state == 12 && !row.saving;
        },
        callback: function(row, reload) {
          unpublishVideo([row.id], reload);
        },
        global: function(selected, reload) {
          unpublishVideo(selected, reload);
        }
      },
      {
        label: $filter('translate')('VIDEOS.CHAPTER_EDIT'),
        condition: function(row) {
          return $scope.rights.chapter && !row.saving && (row.state == 11 || row.state == 12);
        },
        callback: function(row) {
          editChapter(row);
        }
      },
      {
        label: $filter('translate')('VIDEOS.RETRY'),
        condition: function(row) {
          return row.state == 0 && !row.saving;
        },
        callback: function(row, reload) {
          retryVideo([row.id], reload);
        }
      },
      {
        label: $filter('translate')('UI.REMOVE'),
        condition: function(row) {
          return $scope.rights.delete &&
            !row.locked &&
            !row.saving &&
            (row.state === 6 || row.state === 11 || row.state === 12 || row.state === 0);
        },
        warningPopup: true,
        callback: function(row, reload) {
          removeRows([row.id], reload);
        },
        global: function(selected, reload) {
          removeRows(selected, reload);
        }
      }
    ];

    // Add upload actions
    for (var i = 0; i < $scope.platforms.length; i++) {
      var platformName = $scope.platforms[i];
      scopeDataTable.actions.push({
        label: $filter('translate')('VIDEOS.UPLOAD_' + platformName.toUpperCase()),
        condition: function(row) {
          return row.state == 6 && !row.saving;
        },
        callback: function(row, reload) {
          startVideoUpload([row.id], this.platform, reload);
        },
        platform: platformName
      });
    }

    scopeEditForm.init = function(row) {
      scopeEditForm.fields = angular.copy(scopeEditForm.fieldsBase);
      angular.forEach(row.properties, function(value, key) {
        var newField = {
          key: key,
          type: 'horizontalExtendInput',
          model: row.properties,
          templateOptions: {
            label: replacePropIdByName(key)
          }
        };
        scopeEditForm.fields.push(newField);
      });
    };

    scopeEditForm.conditionToggleDetail = function(row) {
      return row.state !== 0;
    };

    scopeEditForm.conditionEditDetail = function(row) {
      return $scope.rights.edit && !row.locked && row.state !== 0;
    };
    scopeEditForm.onSubmit = function(model) {
      return saveVideo(model);
    };

    // Listen to destroy event on the view to update
    $scope.$on('$destroy', function() {
      $interval.cancel(pollVideosPromise);
    });
  }

  app.controller('VideoController', VideoController);
  VideoController.$inject = [
    '$scope',
    '$filter',
    '$location',
    '$window',
    '$interval',
    '$timeout',
    'entityService',
    'publishService',
    'properties',
    'categories',
    'platforms',
    'jsonPath',
    'tableReloadEventService',
    'i18nService'
  ];

})(angular.module('ov.publish'));
<commit_msg>remove dead code<commit_after>'use strict';

(function(app) {

  /**
   * Defines the video controller for the videos page.
   */
  function VideoController(
  $scope,
  $filter,
  $location,
  $window,
  $interval,
  $timeout,
  entityService,
  publishService,
  properties,
  categories,
  platforms,
  jsonPath,
  tableReloadEventService,
  i18nService) {

    $scope.properties = properties.data.entities;

    // Replace Id in Video by the name of the category
    // Category Id can be overwritten, it is only for display purpose
    $scope.categories = categories.data.taxonomy;
    $scope.platforms = platforms.data.platforms;

    /*
     *
     * RIGHTS
     *
     */
    $scope.rights = {};
    $scope.rights.edit = $scope.checkAccess('update-video');
    $scope.rights.delete = $scope.checkAccess('delete-video');
    $scope.rights.publish = $scope.checkAccess('publish-video');
    $scope.rights.chapter = $scope.checkAccess('chapter-video');

    /*
     * FORM EDIT
     */
    var scopeEditForm = $scope.editFormContainer = {};
    scopeEditForm.model = {};
    scopeEditForm.pendingEdition = false;

    /*
     * DATATABLE
     */
    var scopeDataTable = $scope.tableContainer = {};

    /**
     * Gets all categories and add a value for "none".
     * @param {String} label The label for "no categories"
     * @return {Array} The list of categories
     */
    function getSelectableCategories(label) {
      return [{
        value: null,
        name: $filter('translate')(label)
      }].concat(publishService.getCategoriesOptions());
    }

    /**
     * Opens a link in a new tab.
     * @param {String} link Destination url
     */
    function goToPath(link) {
      $window.open(link, '_blank');
    }

    /**
     * Opens an alert to display HTML code to share the video.
     * @param {Object} video The video to share
     */
    function shareCode(video) {
      $scope.$emit('setAlert', 'info', [
        $filter('translate')('VIDEOS.SHARECODE'),
        '

',
        '<iframe width="480" height="270" ',
        'src="' + $location.protocol() + '://' + $location.host() + ':' + $location.port() + video.link,
        '?fullscreen&lang=' + i18nService.getLanguage() + '"',
        ' frameborder="0"></iframe>',
        '
'
      ].join(''), 0);
    }

    // Iterate through the list of videos, if at least one video
    // is pending, poll each 30 seconds to be informed of
    // its status
    var pollVideosPromise = $interval(function() {
      if (!scopeEditForm.pendingEdition) {
        $scope.$emit('setAlert', 'info', $filter('translate')('VIDEOS.RELOAD'), 4000);
        entityService.deleteCache(scopeDataTable.entityType);
        tableReloadEventService.broadcast();
      }
    }, 30000);

    /**
     * Retries a video which is on error.
     * @param {Array} videos The list of videos to work on
     * @param {Function} reload Function to reload the datatable
     */
    function retryVideo(videos, reload) {
      publishService.retryVideo(videos.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.RETRY_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Asks server to start uploading the video to the chosen platform.
     * @param {Array} videos The list of videos to work on
     * @param {String} platformName The name of platform to upload to
     * @param {Function} reload Function to reload the datatable
     */
    function startVideoUpload(videos, platformName, reload) {
      publishService.startVideoUpload(videos.join(','), platformName)
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.UPLOAD_START_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Publishes a list of videos.
     * @param {Array} videos The list of video ids to publish
     * @param {Function} reload Function to reload the datatable
     */
    function publishVideo(videos, reload) {
      publishService.publishVideo(videos.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.PUBLISHED_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Unpublishes a list of videos.
     * @param {Array} videos The list of video ids to unpublish
     * @param {Function} reload Function to reload the datatable
     */
    function unpublishVideo(videos, reload) {
      publishService.unpublishVideo(videos.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.UNPUBLISHED_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Removes a list of videos.
     * @param {Array} selected The list of video ids to remove
     * @param {Function} reload Function to reload the datatable
     */
    function removeRows(selected, reload) {
      entityService.removeEntity('video', selected.join(','))
        .then(function() {
          $scope.$emit('setAlert', 'success', $filter('translate')('VIDEOS.REMOVE_SUCCESS'), 4000);
          reload();
        });
    }

    /**
     * Saves video information.
     * @param {Object} video Video data
     */
    function saveVideo(video) {
      return entityService.updateEntity('video', video.id, {
        title: video.title,
        description: video.description,
        properties: video.properties,
        category: video.category
      }).then(function() {
        scopeEditForm.pendingEdition = false;
      });
    }

    /**
     * Routes to chapters edition.
     * @param {Object} video The video to edit
     */
    function editChapter(video) {
      $location.path('/publish/video/' + video.id);
    }

    /**
     * Gets property name by id.
     * @param {String} id Id of the property
     * @return {String} The property name
     */
    function replacePropIdByName(id) {
      var found = -1;
      for (var i = 0; i < $scope.properties.length && found < 0; i++) {
        var value = $scope.properties[i];
        if (value.id == String(id)) {
          found = i;
          break;
        }
      }

      if (found != -1)
        return $scope.properties[found].name;
      else
        return id;
    }

    /*
     * FORM EDIT
     */
    scopeEditForm.entityType = 'video';

    var categoriesfield = getSelectableCategories('UI.NONE');
    scopeEditForm.fieldsBase = [
      {
        key: 'title',
        type: 'horizontalExtendInput',
        templateOptions: {
          label: $filter('translate')('VIDEOS.ATTR_TITLE'),
          required: true
        }
      },
      {
        key: 'description',
        type: 'horizontalExtendInput',
        templateOptions: {
          label: $filter('translate')('VIDEOS.ATTR_DESCRIPTION'),
          required: true
        }
      },
      {
        key: 'category',
        type: 'horizontalExtendSelect',
        templateOptions: {
          label: $filter('translate')('VIDEOS.ATTR_CATEGORY'),
          options: categoriesfield
        }
      }
    ];

    /*
     *
     * DATATABLE
     */
    scopeDataTable.entityType = 'video';
    scopeDataTable.cellTheme = '/publish/be/views/partial/publishCells.html';

    scopeDataTable.init = {
      sortBy: 'date',
      sortOrder: 'dsc'
    };
    var categoriesFilter = getSelectableCategories('UI.ALL');
    scopeDataTable.filterBy = [
      {
        key: 'title',
        value: '',
        label: $filter('translate')('VIDEOS.TITLE_FILTER')
      }, {
        key: 'description',
        value: '',
        label: $filter('translate')('VIDEOS.DESCRIPTION_FILTER')
      }, {
        key: 'date',
        type: 'date',
        value: '',
        label: $filter('translate')('VIDEOS.DATE_FILTER')
      }, {
        key: 'category',
        type: 'select',
        value: null,
        label: $filter('translate')('VIDEOS.CATEGORY_FILTER'),

        /*
         * [{
         "value": 'id',
         "name": 'title',
         "children" : 'id1,id2,id3'
         },
         ...
         ];
         */
        options: categoriesFilter,

        // if enable filter will filter with the selectId AND additionnal id set in the "children" key of each options
        filterWithChildren: true
      }
    ];
    scopeDataTable.header = [
      {
        key: 'title',
        name: $filter('translate')('VIDEOS.NAME_COLUMN'),
        class: ['col-xs-12 col-sm-5']
      },
      {
        key: 'date',
        type: 'date',
        name: $filter('translate')('VIDEOS.DATE_COLUMN'),
        class: ['hidden-xs col-sm-1']
      },
      {
        key: 'category',
        type: 'category',
        name: $filter('translate')('VIDEOS.CATEGORY_COLUMN'),
        class: ['hidden-xs col-sm-3']
      },
      {
        key: 'state',
        type: 'status',
        name: $filter('translate')('VIDEOS.STATUS_COLUMN'),
        class: ['hidden-xs col-sm-2']
      },
      {
        key: 'action',
        name: $filter('translate')('UI.ACTIONS_COLUMN'),
        class: [' hidden-xs col-sm-1']
      }];
    scopeDataTable.actions = [
      {
        label: $filter('translate')('UI.VIEW'),
        condition: function(row) {
          return row.state == 12;
        },
        callback: function(row) {
          goToPath(row.link);
        }
      },
      {
        label: $filter('translate')('UI.SHARE'),
        condition: function(row) {
          return row.state == 12;
        },
        callback: function(row) {
          shareCode(row);
        }
      },
      {
        label: $filter('translate')('VIDEOS.PUBLISH'),
        condition: function(row) {
          return $scope.rights.publish && row.state == 11 && !row.saving;
        },
        callback: function(row, reload) {
          publishVideo([row.id], reload);
        },
        global: function(selected, reload) {
          publishVideo(selected, reload);
        }
      },
      {
        label: $filter('translate')('VIDEOS.UNPUBLISH'),
        condition: function(row) {
          return $scope.rights.publish && row.state == 12 && !row.saving;
        },
        callback: function(row, reload) {
          unpublishVideo([row.id], reload);
        },
        global: function(selected, reload) {
          unpublishVideo(selected, reload);
        }
      },
      {
        label: $filter('translate')('VIDEOS.CHAPTER_EDIT'),
        condition: function(row) {
          return $scope.rights.chapter && !row.saving && (row.state == 11 || row.state == 12);
        },
        callback: function(row) {
          editChapter(row);
        }
      },
      {
        label: $filter('translate')('VIDEOS.RETRY'),
        condition: function(row) {
          return row.state == 0 && !row.saving;
        },
        callback: function(row, reload) {
          retryVideo([row.id], reload);
        }
      },
      {
        label: $filter('translate')('UI.REMOVE'),
        condition: function(row) {
          return $scope.rights.delete &&
            !row.locked &&
            !row.saving &&
            (row.state === 6 || row.state === 11 || row.state === 12 || row.state === 0);
        },
        warningPopup: true,
        callback: function(row, reload) {
          removeRows([row.id], reload);
        },
        global: function(selected, reload) {
          removeRows(selected, reload);
        }
      }
    ];

    // Add upload actions
    for (var i = 0; i < $scope.platforms.length; i++) {
      var platformName = $scope.platforms[i];
      scopeDataTable.actions.push({
        label: $filter('translate')('VIDEOS.UPLOAD_' + platformName.toUpperCase()),
        condition: function(row) {
          return row.state == 6 && !row.saving;
        },
        callback: function(row, reload) {
          startVideoUpload([row.id], this.platform, reload);
        },
        platform: platformName
      });
    }

    scopeEditForm.init = function(row) {
      scopeEditForm.fields = angular.copy(scopeEditForm.fieldsBase);
      angular.forEach(row.properties, function(value, key) {
        var newField = {
          key: key,
          type: 'horizontalExtendInput',
          model: row.properties,
          templateOptions: {
            label: replacePropIdByName(key)
          }
        };
        scopeEditForm.fields.push(newField);
      });
    };

    scopeEditForm.conditionToggleDetail = function(row) {
      return row.state !== 0;
    };

    scopeEditForm.conditionEditDetail = function(row) {
      return $scope.rights.edit && !row.locked && row.state !== 0;
    };
    scopeEditForm.onSubmit = function(model) {
      return saveVideo(model);
    };

    // Listen to destroy event on the view to update
    $scope.$on('$destroy', function() {
      $interval.cancel(pollVideosPromise);
    });
  }

  app.controller('VideoController', VideoController);
  VideoController.$inject = [
    '$scope',
    '$filter',
    '$location',
    '$window',
    '$interval',
    '$timeout',
    'entityService',
    'publishService',
    'properties',
    'categories',
    'platforms',
    'jsonPath',
    'tableReloadEventService',
    'i18nService'
  ];

})(angular.module('ov.publish'));
<|endoftext|>


<commit_before>import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';
import TranslationService from 'resources/services/translation-service';
import LocationService from 'resources/services/location-service';
import RemarkService from 'resources/services/remark-service';
import FiltersService from 'resources/services/filters-service';
import LoaderService from 'resources/services/loader-service';
import ToastService from 'resources/services/toast-service';
import AuthService from 'resources/services/auth-service';
import UserService from 'resources/services/user-service';
import SignalRService from 'resources/services/signalr-service';
import LogService from 'resources/services/log-service';
import {EventAggregator} from 'aurelia-event-aggregator';

@inject(Router, TranslationService, LocationService, RemarkService,
FiltersService, LoaderService, ToastService, AuthService,
UserService, SignalRService, LogService, EventAggregator)
export class Remarks {
  constructor(router, translationService, location,
  remarkService, filtersService, loader, toast, authService,
  userService, signalRService, logService, eventAggregator) {
    this.router = router;
    this.translationService = translationService;
    this.location = location;
    this.remarkService = remarkService;
    this.filtersService = filtersService;
    this.loader = loader;
    this.toast = toast;
    this.authService = authService;
    this.userService = userService;
    this.signalR = signalRService;
    this.log = logService;
    this.eventAggregator = eventAggregator;
    this.files = [];
    this.query = {
      radius: this.filtersService.filters.radius,
      longitude: this.location.current.longitude,
      latitude: this.location.current.latitude,
      categories: encodeURI(this.filtersService.filters.categories),
      state: this.filtersService.filters.state,
      disliked: this.filtersService.filters.disliked
    };
    this.page = 1;
    this.results = 25;
    this.remarks = [];
    this.mapRemarks = [];
    this.selectedRemark = null;
    this.mapLoadedSubscription = null;
    this.signalR.initialize();
    this.loadingRemarks = false;
    this.mapEnabled = this.filtersService.filters.map.enabled;
  }

  async activate(params) {
    this.location.startUpdating();
    this.account = {userId: ''};
    this.isAuthenticated = this.authService.isLoggedIn;
    if (this.isAuthenticated) {
      this.account = await this.userService.getAccount();
    }
    this.filtersEnabled = this.isAuthenticated;
    this.createRemarkEnabled = this.isAuthenticated;
    this.selectedRemarkId = params.id;
    this.log.trace('remarks_activated', {
      filters: this.filtersService.filters,
      location: this.location.current
    });
  }

  get resetPositionEnabled() {
    return this.isAuthenticated && this.mapEnabled;
  }

  async attached() {
    this.fileInput = document.getElementById('file');
    $('#file').change(async () => {
      this.image = this.files[0];
    });
    this.mapLoadedSubscription = await this.subscribeMapLoaded();
    this.remarkCreatedSubscription = await this.subscribeRemarkCreated();
    this.remarkResolvedSubscription = await this.subscribeRemarkResolved();
    this.remarkDeletedSubscription = await this.subscribeRemarkDeleted();
    this.remarkPhotosAddedSubscription = await this.subscribeRemarkPhotosAdded();
    this.remarkPhotoRemovedSubscription = await this.subscribeRemarkPhotoRemoved();
    await this.browseForList(this.page);
    this.log.trace('remarks_attached', {filters: this.filtersService.filters});
  }

  detached() {
    this.mapLoadedSubscription.dispose();
    this.remarkCreatedSubscription.dispose();
    this.remarkResolvedSubscription.dispose();
    this.remarkDeletedSubscription.dispose();
    this.remarkPhotosAddedSubscription.dispose();
    this.remarkPhotoRemovedSubscription.dispose();
  }

  async browseForMap() {
    this.query.results = this.filtersService.filters.results;
    this.query.radius = this.filtersService.filters.radius;
    this.mapRemarks = await this.browse(this.query);
  }

  async browseForList(page, results, clear = false) {
    let query = {
      radius: 0,
      longitude: this.location.current.longitude,
      latitude: this.location.current.latitude,
      categories: encodeURI(this.filtersService.filters.categories),
      state: this.filtersService.filters.state,
      disliked: this.filtersService.filters.disliked,
      page: page || 0,
      results: results || 25
    };
    let remarks = await this.browse(query, !clear);
    if (clear) {
      this.remarks = [];
    }
    remarks.forEach(remark => {
      if (this.remarks.includes(remark)) {
        return;
      }
      this.remarks.push(remark);
    }, this);
  }

  async browse(query, cache = true) {
    query.authorId = '';
    if (this.filtersService.filters.type === 'mine') {
      query.authorId = this.account.userId;
    }
    let remarks = await this.remarkService.browse(query, cache);
    remarks.forEach(remark => this.processRemark(remark), this);

    return remarks;
  }

  async refreshList() {
    if (this.loadingRemarks) {
      return;
    }
    this.loadingRemarks = true;
    this.page = 1;
    await this.browseForList(this.page, this.results, true);
    this.loader.hide();
    this.loadingRemarks = false;
  }

  async loadMore() {
    if (this.loadingRemarks) {
      return;
    }
    if (this.page * this.results > this.remarks.length) {
      return;
    }
    this.loadingRemarks = true;
    this.loader.display();
    this.page++;
    await this.browseForList(this.page);
    this.loader.hide();
    this.loadingRemarks = false;
  }

  processRemark(remark) {
    remark.url = this.router.generate('remark', {id: remark.id});
    remark.selected = remark.id === this.selectedRemarkId;
    let latitude = remark.location.coordinates[1];
    let longitude = remark.location.coordinates[0];
    remark.distance = this.location.calculateDistance({
      latitude: latitude,
      longitude: longitude
    });

    if (!remark.selected) {
      return remark;
    }

    this.filtersService.setCenter({latitude, longitude});
    this.filtersService.setMapEnabled(true);

    return remark;
  }

  navigateToCreateRemark() {
    this.router.navigate('remarks/create');
  }

  async radiusChanged(radius, center) {
    this.filtersService.setRadius(radius);
    this.query.longitude = center.lng();
    this.query.latitude = center.lat();
    await this.browseForMap();
  }

  get mapEnabled() {
    return this.filtersService.filters.map.enabled;
  }

  set mapEnabled(value) {
    this.filtersService.setMapEnabled(value);
  }

  toggleMapEnabled() {
    this.mapEnabled = !this.mapEnabled;
  }

  resetPosition() {
    this.location.getLocation(l => {
      this.filtersService.setDefaultCenter({latitude: l.coords.latitude, longitude: l.coords.longitude});
      this.filtersService.setCenter(this.filtersService.filters.defaultCenter);
      this.eventAggregator.publish('location:reset-center', this.filtersService.filters.center);
    });
  }

  async subscribeMapLoaded() {
    return await this.eventAggregator
      .subscribe('map:loaded', async response => {
        this.loader.display();
        if (this.filtersService.filters.map.follow) {
          this.resetPosition();
        }
        await this.browseForMap();
        this.loader.hide();
      });
  }

  async subscribeRemarkCreated() {
    return await this.eventAggregator
      .subscribe('remark:created', async message => {
        let location = {
          latitude: message.location.coordinates[1],
          longitude: message.location.coordinates[0]
        };
        let remark = this.processRemark(message);
        if (this.location.isInRange(location, this.filtersService.filters.radius)) {
          this.mapRemarks = this.insertRemark(this.mapRemarks, remark);
        }
        let lastRemark = this.remarks.length > 0
          ? this.remarks[this.remarks.length - 1]
          : null;
        if (lastRemark && this.location.isInRange(location, lastRemark.distance)) {
          this.remarks = this.insertRemark(this.remarks, remark);
        }
      });
  }

  async subscribeRemarkResolved() {
    return await this.eventAggregator
      .subscribe('remark:resolved', async message => {
        this.remarks = this.markAsResolved(this.remarks, message);
        this.mapRemarks = this.markAsResolved(this.mapRemarks, message);
      });
  }

  async subscribeRemarkDeleted() {
    return await this.eventAggregator
      .subscribe('remark:deleted', async message => {
        this.remarks = this.removeRemark(this.remarks, message.remarkId);
        this.mapRemarks = this.removeRemark(this.mapRemarks, message.remarkId);
      });
  }

  async subscribeRemarkPhotosAdded() {
    return await this.eventAggregator
      .subscribe('remark:photo_added', async message => {
        this.remarks = this.updatePhotos(this.remarks, message);
      });
  }

  async subscribeRemarkPhotoRemoved() {
    return await this.eventAggregator
      .subscribe('remark:photo_removed', async message => {
        this.remarks = this.removeAndUpdateRemarkPhoto(this.remarks, message);
      });
  }

  removeAndUpdateRemarkPhoto(remarks, message) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    let index = remarks.findIndex(r => r.id === message.remarkId);
    if (index < 0) {
      return remarks;
    }
    let remark = remarks[index];
    let photoUri = remark.smallPhotoUrl.split('/');
    let photoName = photoUri[photoUri.length - 1];
    if (Array.isArray(message.photos) && message.photos.includes(photoName)) {
      remark.smallPhotoUrl = '';
      remarks[index] = remark;
      return Array.from(remarks);
    }

    return remarks;
  }

  updatePhotos(remarks, message) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    if (message.photosCount > 1) {
      return remarks;
    }

    let index = remarks.findIndex(r => r.id === message.remarkId);
    if (index < 0) {
      return remarks;
    }

    let remark = remarks[index];
    let photo = message.newPhotos.find(p => p.size === 'small');
    remark.smallPhotoUrl = photo.url;
    remarks[index] = remark;

    return Array.from(remarks);
  }

  insertRemark(remarks, remark) {
    if (Array.isArray(remarks) === false) {
      remarks = [];
    }
    if (remarks.includes(remark)) {
      return remarks;
    }
    remarks.push(remark);

    return Array.from(remarks);
  }

  markAsResolved(remarks, message) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    let index = remarks.findIndex(r => r.id === message.remarkId);
    if (index < 0) {
      return remarks;
    }
    let remark = remarks[index];
    remark.resolved = true;
    remark.resolver = {
      name: message.resolver,
      userId: message.resolverId
    };
    remark.resolvedAt = message.resolvedAt;
    remarks[index] = remark;

    return Array.from(remarks);
  }

  removeRemark(remarks, remarkId) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    let index = remarks.findIndex(r => r.id === remarkId);
    if (index < 0) {
      return remarks;
    }
    remarks.splice(index, 1);

    return Array.from(remarks);
  }
}
<commit_msg>always show nav bar when map enabled<commit_after>import {inject} from 'aurelia-framework';
import {Router} from 'aurelia-router';
import TranslationService from 'resources/services/translation-service';
import LocationService from 'resources/services/location-service';
import RemarkService from 'resources/services/remark-service';
import FiltersService from 'resources/services/filters-service';
import LoaderService from 'resources/services/loader-service';
import ToastService from 'resources/services/toast-service';
import AuthService from 'resources/services/auth-service';
import UserService from 'resources/services/user-service';
import SignalRService from 'resources/services/signalr-service';
import LogService from 'resources/services/log-service';
import {EventAggregator} from 'aurelia-event-aggregator';

@inject(Router, TranslationService, LocationService, RemarkService,
FiltersService, LoaderService, ToastService, AuthService,
UserService, SignalRService, LogService, EventAggregator)
export class Remarks {
  constructor(router, translationService, location,
  remarkService, filtersService, loader, toast, authService,
  userService, signalRService, logService, eventAggregator) {
    this.router = router;
    this.translationService = translationService;
    this.location = location;
    this.remarkService = remarkService;
    this.filtersService = filtersService;
    this.loader = loader;
    this.toast = toast;
    this.authService = authService;
    this.userService = userService;
    this.signalR = signalRService;
    this.log = logService;
    this.eventAggregator = eventAggregator;
    this.files = [];
    this.query = {
      radius: this.filtersService.filters.radius,
      longitude: this.location.current.longitude,
      latitude: this.location.current.latitude,
      categories: encodeURI(this.filtersService.filters.categories),
      state: this.filtersService.filters.state,
      disliked: this.filtersService.filters.disliked
    };
    this.page = 1;
    this.results = 25;
    this.remarks = [];
    this.mapRemarks = [];
    this.selectedRemark = null;
    this.mapLoadedSubscription = null;
    this.signalR.initialize();
    this.loadingRemarks = false;
    this.mapEnabled = this.filtersService.filters.map.enabled;
  }

  async activate(params) {
    this.location.startUpdating();
    this.account = {userId: ''};
    this.isAuthenticated = this.authService.isLoggedIn;
    if (this.isAuthenticated) {
      this.account = await this.userService.getAccount();
    }
    this.filtersEnabled = this.isAuthenticated;
    this.createRemarkEnabled = this.isAuthenticated;
    this.selectedRemarkId = params.id;
    this.log.trace('remarks_activated', {
      filters: this.filtersService.filters,
      location: this.location.current
    });
  }

  get resetPositionEnabled() {
    return this.isAuthenticated && this.mapEnabled;
  }

  async attached() {
    this.fileInput = document.getElementById('file');
    $('#file').change(async () => {
      this.image = this.files[0];
    });
    this.mapLoadedSubscription = await this.subscribeMapLoaded();
    this.remarkCreatedSubscription = await this.subscribeRemarkCreated();
    this.remarkResolvedSubscription = await this.subscribeRemarkResolved();
    this.remarkDeletedSubscription = await this.subscribeRemarkDeleted();
    this.remarkPhotosAddedSubscription = await this.subscribeRemarkPhotosAdded();
    this.remarkPhotoRemovedSubscription = await this.subscribeRemarkPhotoRemoved();
    await this.browseForList(this.page);
    this.log.trace('remarks_attached', {filters: this.filtersService.filters});
  }

  detached() {
    this.mapLoadedSubscription.dispose();
    this.remarkCreatedSubscription.dispose();
    this.remarkResolvedSubscription.dispose();
    this.remarkDeletedSubscription.dispose();
    this.remarkPhotosAddedSubscription.dispose();
    this.remarkPhotoRemovedSubscription.dispose();
  }

  async browseForMap() {
    this.query.results = this.filtersService.filters.results;
    this.query.radius = this.filtersService.filters.radius;
    this.mapRemarks = await this.browse(this.query);
  }

  async browseForList(page, results, clear = false) {
    let query = {
      radius: 0,
      longitude: this.location.current.longitude,
      latitude: this.location.current.latitude,
      categories: encodeURI(this.filtersService.filters.categories),
      state: this.filtersService.filters.state,
      disliked: this.filtersService.filters.disliked,
      page: page || 0,
      results: results || 25
    };
    let remarks = await this.browse(query, !clear);
    if (clear) {
      this.remarks = [];
    }
    remarks.forEach(remark => {
      if (this.remarks.includes(remark)) {
        return;
      }
      this.remarks.push(remark);
    }, this);
  }

  async browse(query, cache = true) {
    query.authorId = '';
    if (this.filtersService.filters.type === 'mine') {
      query.authorId = this.account.userId;
    }
    let remarks = await this.remarkService.browse(query, cache);
    remarks.forEach(remark => this.processRemark(remark), this);

    return remarks;
  }

  async refreshList() {
    if (this.loadingRemarks) {
      return;
    }
    this.loadingRemarks = true;
    this.page = 1;
    await this.browseForList(this.page, this.results, true);
    this.loader.hide();
    this.loadingRemarks = false;
  }

  async loadMore() {
    if (this.loadingRemarks) {
      return;
    }
    if (this.page * this.results > this.remarks.length) {
      return;
    }
    this.loadingRemarks = true;
    this.loader.display();
    this.page++;
    await this.browseForList(this.page);
    this.loader.hide();
    this.loadingRemarks = false;
  }

  processRemark(remark) {
    remark.url = this.router.generate('remark', {id: remark.id});
    remark.selected = remark.id === this.selectedRemarkId;
    let latitude = remark.location.coordinates[1];
    let longitude = remark.location.coordinates[0];
    remark.distance = this.location.calculateDistance({
      latitude: latitude,
      longitude: longitude
    });

    if (!remark.selected) {
      return remark;
    }

    this.filtersService.setCenter({latitude, longitude});
    this.filtersService.setMapEnabled(true);

    return remark;
  }

  navigateToCreateRemark() {
    this.router.navigate('remarks/create');
  }

  async radiusChanged(radius, center) {
    this.filtersService.setRadius(radius);
    this.query.longitude = center.lng();
    this.query.latitude = center.lat();
    await this.browseForMap();
  }

  get mapEnabled() {
    return this.filtersService.filters.map.enabled;
  }

  set mapEnabled(value) {
    this.filtersService.setMapEnabled(value);
  }

  toggleMapEnabled() {
    this.mapEnabled = !this.mapEnabled;
    if (this.mapEnabled) {
      this.scrollToTop();
    }
  }

  resetPosition() {
    this.location.getLocation(l => {
      this.filtersService.setDefaultCenter({latitude: l.coords.latitude, longitude: l.coords.longitude});
      this.filtersService.setCenter(this.filtersService.filters.defaultCenter);
      this.eventAggregator.publish('location:reset-center', this.filtersService.filters.center);
    });
  }

  async subscribeMapLoaded() {
    return await this.eventAggregator
      .subscribe('map:loaded', async response => {
        this.loader.display();
        if (this.filtersService.filters.map.follow) {
          this.resetPosition();
        }
        await this.browseForMap();
        this.loader.hide();
      });
  }

  async subscribeRemarkCreated() {
    return await this.eventAggregator
      .subscribe('remark:created', async message => {
        let location = {
          latitude: message.location.coordinates[1],
          longitude: message.location.coordinates[0]
        };
        let remark = this.processRemark(message);
        if (this.location.isInRange(location, this.filtersService.filters.radius)) {
          this.mapRemarks = this.insertRemark(this.mapRemarks, remark);
        }
        let lastRemark = this.remarks.length > 0
          ? this.remarks[this.remarks.length - 1]
          : null;
        if (lastRemark && this.location.isInRange(location, lastRemark.distance)) {
          this.remarks = this.insertRemark(this.remarks, remark);
        }
      });
  }

  async subscribeRemarkResolved() {
    return await this.eventAggregator
      .subscribe('remark:resolved', async message => {
        this.remarks = this.markAsResolved(this.remarks, message);
        this.mapRemarks = this.markAsResolved(this.mapRemarks, message);
      });
  }

  async subscribeRemarkDeleted() {
    return await this.eventAggregator
      .subscribe('remark:deleted', async message => {
        this.remarks = this.removeRemark(this.remarks, message.remarkId);
        this.mapRemarks = this.removeRemark(this.mapRemarks, message.remarkId);
      });
  }

  async subscribeRemarkPhotosAdded() {
    return await this.eventAggregator
      .subscribe('remark:photo_added', async message => {
        this.remarks = this.updatePhotos(this.remarks, message);
      });
  }

  async subscribeRemarkPhotoRemoved() {
    return await this.eventAggregator
      .subscribe('remark:photo_removed', async message => {
        this.remarks = this.removeAndUpdateRemarkPhoto(this.remarks, message);
      });
  }

  removeAndUpdateRemarkPhoto(remarks, message) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    let index = remarks.findIndex(r => r.id === message.remarkId);
    if (index < 0) {
      return remarks;
    }
    let remark = remarks[index];
    let photoUri = remark.smallPhotoUrl.split('/');
    let photoName = photoUri[photoUri.length - 1];
    if (Array.isArray(message.photos) && message.photos.includes(photoName)) {
      remark.smallPhotoUrl = '';
      remarks[index] = remark;
      return Array.from(remarks);
    }

    return remarks;
  }

  updatePhotos(remarks, message) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    if (message.photosCount > 1) {
      return remarks;
    }

    let index = remarks.findIndex(r => r.id === message.remarkId);
    if (index < 0) {
      return remarks;
    }

    let remark = remarks[index];
    let photo = message.newPhotos.find(p => p.size === 'small');
    remark.smallPhotoUrl = photo.url;
    remarks[index] = remark;

    return Array.from(remarks);
  }

  insertRemark(remarks, remark) {
    if (Array.isArray(remarks) === false) {
      remarks = [];
    }
    if (remarks.includes(remark)) {
      return remarks;
    }
    remarks.push(remark);

    return Array.from(remarks);
  }

  markAsResolved(remarks, message) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    let index = remarks.findIndex(r => r.id === message.remarkId);
    if (index < 0) {
      return remarks;
    }
    let remark = remarks[index];
    remark.resolved = true;
    remark.resolver = {
      name: message.resolver,
      userId: message.resolverId
    };
    remark.resolvedAt = message.resolvedAt;
    remarks[index] = remark;

    return Array.from(remarks);
  }

  removeRemark(remarks, remarkId) {
    if (Array.isArray(remarks) === false) {
      return [];
    }
    let index = remarks.findIndex(r => r.id === remarkId);
    if (index < 0) {
      return remarks;
    }
    remarks.splice(index, 1);

    return Array.from(remarks);
  }

  scrollToTop() {
    window.scrollTo(0, 0);
  }
}
<|endoftext|>


<commit_before>
/*

  Views.js

*/

var Utils    = require("./utils"),
    Metadata = require("./metadata"),
    fs       = require("fs"),
    cons     = require("consolidate"),
    _        = require("lodash/core"),
    constant = require("lodash/constant"),
    express  = require("express"),
    Promise  = require("bluebird"),
    readFile = Promise.promisify(fs.readFile);

function addEngines(router, engines) {
  for (var ext in engines)
    router.engine(ext, makeEngine(engines[ext]));

  // Add standard html engine if not already added
  if (!router.engines["html"]) router.engine("html", makeEngine("html"));
}

function makeEngine(pipeline) {

  // Simple temlating
  if (-1 === pipeline.indexOf(">") && "html" !== pipeline) return cons[pipeline];

  return function(path, options, fn) {

    var promise = readFile(path, "utf8"),
        engines = (pipeline) ? pipeline.split(">") : [],
        engine;

    // Chain through engines
    while (engine = engines.shift())
      if ("html" !== engine) (function(eng) {
        promise = promise.then(function(str) { return cons[eng].render(str, options); })
      })(engine);

    // Return result
    promise.then(function(str) { fn(null, str ); });

  };
}

function addHtmlEngine(router) {
  if ("undefined" === typeof router.engines["html"]) {
    router.engine("html", function (path, options, fn) {
      fn(null, fs.readFileSync(path, "utf8"));
    });
  }
}

function makeDataQuery(db, queries, context) {

  if (queries) {

    var key,
        keys = _.keys(context),
        re   = new RegExp("<(" + keys.join("|") + ")>", "gi"),
        match;

    queries.forEach(function(query) {
      for (key in query.filters || {}) {
        if (match = query.filters[key].match(re)) {
          match = match[0];
          query.filters[key] = query.filters[key].replace(new RegExp(match, "g"), context[match.slice(1, match.length-1)]);
        }
      }
    });

    return function() { return db.queries(queries); }
  } else
    return constant({});

}

function makeRoutes(route, file, query) {
  return Utils.makeUrls(route, file, ".html");
}

module.exports = function(db, options) {

  var router = express();

  // Set defaults
  options = _.extend({ path : "./views", url : "/" }, { engines : global.engines }, options);
  addEngines(router, options.engines);

  // Walk through each file
  Utils.eachFile(options.path, function(file) {

    var metadata  = Metadata.extract(Utils.readFileHead(file.path, 500)),
        dataQuery = makeDataQuery(db, metadata.data),
        handler;

    handler = function(req, res) {

      router.render(file.path, dataQuery() || {}, function(err, html) {

        // Set cookies
        for (var key in metadata.cookies || {})
          res.cookie(key, metadata.cookies[key]);

        // Set session
        for (var key in metadata.session || {})
          req.session[key] = metadata.session[key];

        // Send response
        res.type("text/html").send(html);

      });
    }

    makeRoutes(options.url, file, metadata.query).forEach(function(slug) {
      router.get(slug, handler);
    });

  });

  return router;

}
<commit_msg>Views: adds basic context to metadata<commit_after>
/*

  Views.js

*/

var Utils    = require("./utils"),
    Metadata = require("./metadata"),
    fs       = require("fs"),
    cons     = require("consolidate"),
    _        = require("lodash/core"),
    constant = require("lodash/constant"),
    express  = require("express"),
    Promise  = require("bluebird"),
    readFile = Promise.promisify(fs.readFile);

function addEngines(router, engines) {
  for (var ext in engines)
    router.engine(ext, makeEngine(engines[ext]));

  // Add standard html engine if not already added
  if (!router.engines["html"]) router.engine("html", makeEngine("html"));
}

function makeEngine(pipeline) {

  // Simple temlating
  if (-1 === pipeline.indexOf(">") && "html" !== pipeline) return cons[pipeline];

  return function(path, options, fn) {

    var promise = readFile(path, "utf8"),
        engines = (pipeline) ? pipeline.split(">") : [],
        engine;

    // Chain through engines
    while (engine = engines.shift())
      if ("html" !== engine) (function(eng) {
        promise = promise.then(function(str) { return cons[eng].render(str, options); })
      })(engine);

    // Return result
    promise.then(function(str) { fn(null, str ); });

  };
}

function addHtmlEngine(router) {
  if ("undefined" === typeof router.engines["html"]) {
    router.engine("html", function (path, options, fn) {
      fn(null, fs.readFileSync(path, "utf8"));
    });
  }
}

function makeDataQuery(db, queries, context) {

  if (queries) {

    var key,
        keys = _.keys(context),
        re   = new RegExp("<(" + keys.join("|") + ")>", "gi"),
        match;

    queries.forEach(function(query) {
      for (key in query.filters || {}) {
        if (match = query.filters[key].match(re)) {
          match = match[0];
          query.filters[key] = query.filters[key].replace(new RegExp(match, "g"), context[match.slice(1, match.length-1)]);
        }
      }
    });

    return function() { return db.queries(queries); }
  } else
    return constant({});

}

function makeRoutes(route, file, query) {
  return Utils.makeUrls(route, file, ".html");
}

module.exports = function(db, options) {

  var router = express();

  // Set defaults
  options = _.extend({ path : "./views", url : "/" }, { engines : global.engines }, options);
  addEngines(router, options.engines);

  // Walk through each file
  Utils.eachFile(options.path, function(file) {

    var metadata = Metadata.extract(Utils.readFileHead(file.path, 500)),
        handler;

    handler = function(req, res) {

      // !!! REPLACE THIS LATER
      var context = _.clone(req.params);
          dataQuery = makeDataQuery(db, metadata.data, context);

      router.render(file.path, dataQuery() || {}, function(err, html) {

        // Set cookies
        for (var key in metadata.cookies || {})
          res.cookie(key, metadata.cookies[key]);

        // Set session
        for (var key in metadata.session || {})
          req.session[key] = metadata.session[key];

        // Send response
        res.type("text/html").send(html);

      });
    }

    makeRoutes(options.url, file, metadata.query).forEach(function(slug) {
      router.get(slug, handler);
    });

  });

  return router;

}
<|endoftext|>