<commit_before>/**
 * @module ol/renderer/canvas/VectorLayer
 */
import {getUid} from '../../util.js';
import ViewHint from '../../ViewHint.js';
import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js';
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
import CanvasLayerRenderer from './Layer.js';
import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js';
import {toString as transformToString, makeScale, makeInverse} from '../../transform.js';

/**
 * @classdesc
 * Canvas renderer for vector layers.
 * @api
 */
class CanvasVectorLayerRenderer extends CanvasLayerRenderer {

  /**
   * @param {import("../../layer/Vector.js").default} vectorLayer Vector layer.
   */
  constructor(vectorLayer) {

    super(vectorLayer);

    /**
     * @private
     * @type {boolean}
     */
    this.dirty_ = false;

    /**
     * @private
     * @type {number}
     */
    this.renderedRevision_ = -1;

    /**
     * @private
     * @type {number}
     */
    this.renderedResolution_ = NaN;

    /**
     * @private
     * @type {import("../../extent.js").Extent}
     */
    this.renderedExtent_ = createEmpty();

    /**
     * @private
     * @type {function(import("../../Feature.js").default, import("../../Feature.js").default): number|null}
     */
    this.renderedRenderOrder_ = null;

    /**
     * @private
     * @type {import("../../render/canvas/ExecutorGroup").default}
     */
    this.replayGroup_ = null;

    /**
     * A new replay group had to be created by `prepareFrame()`
     * @type {boolean}
     */
    this.replayGroupChanged = true;
  }

  /**
   * @inheritDoc
   */
  useContainer(target, transform, opacity) {
    if (opacity < 1) {
      target = null;
    }
    super.useContainer(target, transform, opacity);
  }

  /**
   * @inheritDoc
   */
  renderFrame(frameState, target) {

    const pixelRatio = frameState.pixelRatio;
    const layerState = frameState.layerStatesArray[frameState.layerIndex];

    // set forward and inverse pixel transforms
    makeScale(this.pixelTransform, 1 / pixelRatio, 1 / pixelRatio);
    makeInverse(this.inversePixelTransform, this.pixelTransform);

    this.useContainer(target, this.pixelTransform, layerState.opacity);
    const context = this.context;
    const canvas = context.canvas;

    const replayGroup = this.replayGroup_;
    if (!replayGroup || replayGroup.isEmpty()) {
      if (!this.containerReused && canvas.width > 0) {
        canvas.width = 0;
      }
      return this.container;
    }

    // resize and clear
    const width = Math.round(frameState.size[0] * pixelRatio);
    const height = Math.round(frameState.size[1] * pixelRatio);
    if (canvas.width != width || canvas.height != height) {
      canvas.width = width;
      canvas.height = height;
      const canvasTransform = transformToString(this.pixelTransform);
      if (canvas.style.transform !== canvasTransform) {
        canvas.style.transform = canvasTransform;
      }
    } else if (!this.containerReused) {
      context.clearRect(0, 0, width, height);
    }

    this.preRender(context, frameState);

    const extent = frameState.extent;
    const viewState = frameState.viewState;
    const projection = viewState.projection;
    const rotation = viewState.rotation;
    const projectionExtent = projection.getExtent();
    const vectorSource = this.getLayer().getSource();

    // clipped rendering if layer extent is set
    const clipExtent = layerState.extent;
    const clipped = clipExtent !== undefined;
    if (clipped) {
      this.clip(context, frameState, clipExtent);
    }

    const viewHints = frameState.viewHints;
    const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);

    const transform = this.getRenderTransform(frameState, width, height, 0);
    const skippedFeatureUids = layerState.managed ? frameState.skippedFeatureUids : {};
    const declutterReplays = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()).getDeclutter() ? {} : null;
    replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays);

    if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) {
      let startX = extent[0];
      const worldWidth = getWidth(projectionExtent);
      let world = 0;
      let offsetX;
      while (startX < projectionExtent[0]) {
        --world;
        offsetX = worldWidth * world;
        const transform = this.getRenderTransform(frameState, width, height, offsetX);
        replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays);
        startX += worldWidth;
      }
      world = 0;
      startX = extent[2];
      while (startX > projectionExtent[2]) {
        ++world;
        offsetX = worldWidth * world;
        const transform = this.getRenderTransform(frameState, width, height, offsetX);
        replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays);
        startX -= worldWidth;
      }
    }
    if (declutterReplays) {
      const viewHints = frameState.viewHints;
      const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
      replayDeclutter(declutterReplays, context, rotation, 1, hifi, frameState.declutterItems);
    }

    if (clipped) {
      context.restore();
    }

    this.postRender(context, frameState);

    const opacity = layerState.opacity;
    const container = this.container;
    if (opacity !== parseFloat(container.style.opacity)) {
      container.style.opacity = opacity === 1 ? '' : opacity;
    }

    return this.container;
  }

  /**
   * @inheritDoc
   */
  forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) {
    if (!this.replayGroup_) {
      return undefined;
    } else {
      const resolution = frameState.viewState.resolution;
      const rotation = frameState.viewState.rotation;
      const layer = /** @type {import("../../layer/Vector").default} */ (this.getLayer());
      /** @type {!Object<string, boolean>} */
      const features = {};
      const result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {},
        /**
         * @param {import("../../Feature.js").FeatureLike} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          const key = getUid(feature);
          if (!(key in features)) {
            features[key] = true;
            return callback(feature, layer);
          }
        }, layer.getDeclutter() ? declutteredFeatures : null);
      return result;
    }
  }

  /**
   * @inheritDoc
   */
  handleFontsChanged() {
    const layer = this.getLayer();
    if (layer.getVisible() && this.replayGroup_) {
      layer.changed();
    }
  }

  /**
   * Handle changes in image style state.
   * @param {import("../../events/Event.js").default} event Image style change event.
   * @private
   */
  handleStyleImageChange_(event) {
    this.renderIfReadyAndVisible();
  }

  /**
   * @inheritDoc
   */
  prepareFrame(frameState) {
    const vectorLayer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer());
    const vectorSource = vectorLayer.getSource();

    const animating = frameState.viewHints[ViewHint.ANIMATING];
    const interacting = frameState.viewHints[ViewHint.INTERACTING];
    const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
    const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();

    if (!this.dirty_ && (!updateWhileAnimating && animating) ||
        (!updateWhileInteracting && interacting)) {
      return true;
    }

    const frameStateExtent = frameState.extent;
    const viewState = frameState.viewState;
    const projection = viewState.projection;
    const resolution = viewState.resolution;
    const pixelRatio = frameState.pixelRatio;
    const vectorLayerRevision = vectorLayer.getRevision();
    const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
    let vectorLayerRenderOrder = vectorLayer.getRenderOrder();

    if (vectorLayerRenderOrder === undefined) {
      vectorLayerRenderOrder = defaultRenderOrder;
    }

    const extent = buffer(frameStateExtent,
      vectorLayerRenderBuffer * resolution);
    const projectionExtent = viewState.projection.getExtent();

    if (vectorSource.getWrapX() && viewState.projection.canWrapX() &&
        !containsExtent(projectionExtent, frameState.extent)) {
      // For the replay group, we need an extent that intersects the real world
      // (-180° to +180°). To support geometries in a coordinate range from -540°
      // to +540°, we add at least 1 world width on each side of the projection
      // extent. If the viewport is wider than the world, we need to add half of
      // the viewport width to make sure we cover the whole viewport.
      const worldWidth = getWidth(projectionExtent);
      const gutter = Math.max(getWidth(extent) / 2, worldWidth);
      extent[0] = projectionExtent[0] - gutter;
      extent[2] = projectionExtent[2] + gutter;
    }

    if (!this.dirty_ &&
        this.renderedResolution_ == resolution &&
        this.renderedRevision_ == vectorLayerRevision &&
        this.renderedRenderOrder_ == vectorLayerRenderOrder &&
        containsExtent(this.renderedExtent_, extent)) {
      this.replayGroupChanged = false;
      return true;
    }

    if (this.replayGroup_) {
      this.replayGroup_.dispose();
    }
    this.replayGroup_ = null;

    this.dirty_ = false;

    const replayGroup = new CanvasBuilderGroup(
      getRenderTolerance(resolution, pixelRatio), extent, resolution,
      pixelRatio, vectorLayer.getDeclutter());

    vectorSource.loadFeatures(extent, resolution, projection);

    /**
     * @param {import("../../Feature.js").default} feature Feature.
     * @this {CanvasVectorLayerRenderer}
     */
    const render = function(feature) {
      let styles;
      const styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resolution);
      }
      if (styles) {
        const dirty = this.renderFeature(
          feature, resolution, pixelRatio, styles, replayGroup);
        this.dirty_ = this.dirty_ || dirty;
      }
    }.bind(this);
    if (vectorLayerRenderOrder) {
      /** @type {Array<import("../../Feature.js").default>} */
      const features = [];
      vectorSource.forEachFeatureInExtent(extent,
        /**
         * @param {import("../../Feature.js").default} feature Feature.
         */
        function(feature) {
          features.push(feature);
        });
      features.sort(vectorLayerRenderOrder);
      for (let i = 0, ii = features.length; i < ii; ++i) {
        render(features[i]);
      }
    } else {
      vectorSource.forEachFeatureInExtent(extent, render);
    }

    const replayGroupInstructions = replayGroup.finish();
    const executorGroup = new ExecutorGroup(extent, resolution,
      pixelRatio, vectorSource.getOverlaps(),
      replayGroupInstructions, vectorLayer.getRenderBuffer());

    this.renderedResolution_ = resolution;
    this.renderedRevision_ = vectorLayerRevision;
    this.renderedRenderOrder_ = vectorLayerRenderOrder;
    this.renderedExtent_ = extent;
    this.replayGroup_ = executorGroup;

    this.replayGroupChanged = true;
    return true;
  }

  /**
   * @param {import("../../Feature.js").default} feature Feature.
   * @param {number} resolution Resolution.
   * @param {number} pixelRatio Pixel ratio.
   * @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
   * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group.
   * @return {boolean} `true` if an image is loading.
   */
  renderFeature(feature, resolution, pixelRatio, styles, builderGroup) {
    if (!styles) {
      return false;
    }
    let loading = false;
    if (Array.isArray(styles)) {
      for (let i = 0, ii = styles.length; i < ii; ++i) {
        loading = renderFeature(
          builderGroup, feature, styles[i],
          getSquaredRenderTolerance(resolution, pixelRatio),
          this.handleStyleImageChange_, this) || loading;
      }
    } else {
      loading = renderFeature(
        builderGroup, feature, styles,
        getSquaredRenderTolerance(resolution, pixelRatio),
        this.handleStyleImageChange_, this);
    }
    return loading;
  }
}

export default CanvasVectorLayerRenderer;
<commit_msg>Squared tolerance does not change for each style.<commit_after>/**
 * @module ol/renderer/canvas/VectorLayer
 */
import {getUid} from '../../util.js';
import ViewHint from '../../ViewHint.js';
import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js';
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
import CanvasLayerRenderer from './Layer.js';
import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js';
import {toString as transformToString, makeScale, makeInverse} from '../../transform.js';

/**
 * @classdesc
 * Canvas renderer for vector layers.
 * @api
 */
class CanvasVectorLayerRenderer extends CanvasLayerRenderer {

  /**
   * @param {import("../../layer/Vector.js").default} vectorLayer Vector layer.
   */
  constructor(vectorLayer) {

    super(vectorLayer);

    /**
     * @private
     * @type {boolean}
     */
    this.dirty_ = false;

    /**
     * @private
     * @type {number}
     */
    this.renderedRevision_ = -1;

    /**
     * @private
     * @type {number}
     */
    this.renderedResolution_ = NaN;

    /**
     * @private
     * @type {import("../../extent.js").Extent}
     */
    this.renderedExtent_ = createEmpty();

    /**
     * @private
     * @type {function(import("../../Feature.js").default, import("../../Feature.js").default): number|null}
     */
    this.renderedRenderOrder_ = null;

    /**
     * @private
     * @type {import("../../render/canvas/ExecutorGroup").default}
     */
    this.replayGroup_ = null;

    /**
     * A new replay group had to be created by `prepareFrame()`
     * @type {boolean}
     */
    this.replayGroupChanged = true;
  }

  /**
   * @inheritDoc
   */
  useContainer(target, transform, opacity) {
    if (opacity < 1) {
      target = null;
    }
    super.useContainer(target, transform, opacity);
  }

  /**
   * @inheritDoc
   */
  renderFrame(frameState, target) {

    const pixelRatio = frameState.pixelRatio;
    const layerState = frameState.layerStatesArray[frameState.layerIndex];

    // set forward and inverse pixel transforms
    makeScale(this.pixelTransform, 1 / pixelRatio, 1 / pixelRatio);
    makeInverse(this.inversePixelTransform, this.pixelTransform);

    this.useContainer(target, this.pixelTransform, layerState.opacity);
    const context = this.context;
    const canvas = context.canvas;

    const replayGroup = this.replayGroup_;
    if (!replayGroup || replayGroup.isEmpty()) {
      if (!this.containerReused && canvas.width > 0) {
        canvas.width = 0;
      }
      return this.container;
    }

    // resize and clear
    const width = Math.round(frameState.size[0] * pixelRatio);
    const height = Math.round(frameState.size[1] * pixelRatio);
    if (canvas.width != width || canvas.height != height) {
      canvas.width = width;
      canvas.height = height;
      const canvasTransform = transformToString(this.pixelTransform);
      if (canvas.style.transform !== canvasTransform) {
        canvas.style.transform = canvasTransform;
      }
    } else if (!this.containerReused) {
      context.clearRect(0, 0, width, height);
    }

    this.preRender(context, frameState);

    const extent = frameState.extent;
    const viewState = frameState.viewState;
    const projection = viewState.projection;
    const rotation = viewState.rotation;
    const projectionExtent = projection.getExtent();
    const vectorSource = this.getLayer().getSource();

    // clipped rendering if layer extent is set
    const clipExtent = layerState.extent;
    const clipped = clipExtent !== undefined;
    if (clipped) {
      this.clip(context, frameState, clipExtent);
    }

    const viewHints = frameState.viewHints;
    const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);

    const transform = this.getRenderTransform(frameState, width, height, 0);
    const skippedFeatureUids = layerState.managed ? frameState.skippedFeatureUids : {};
    const declutterReplays = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer()).getDeclutter() ? {} : null;
    replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays);

    if (vectorSource.getWrapX() && projection.canWrapX() && !containsExtent(projectionExtent, extent)) {
      let startX = extent[0];
      const worldWidth = getWidth(projectionExtent);
      let world = 0;
      let offsetX;
      while (startX < projectionExtent[0]) {
        --world;
        offsetX = worldWidth * world;
        const transform = this.getRenderTransform(frameState, width, height, offsetX);
        replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays);
        startX += worldWidth;
      }
      world = 0;
      startX = extent[2];
      while (startX > projectionExtent[2]) {
        ++world;
        offsetX = worldWidth * world;
        const transform = this.getRenderTransform(frameState, width, height, offsetX);
        replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel, undefined, declutterReplays);
        startX -= worldWidth;
      }
    }
    if (declutterReplays) {
      const viewHints = frameState.viewHints;
      const hifi = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
      replayDeclutter(declutterReplays, context, rotation, 1, hifi, frameState.declutterItems);
    }

    if (clipped) {
      context.restore();
    }

    this.postRender(context, frameState);

    const opacity = layerState.opacity;
    const container = this.container;
    if (opacity !== parseFloat(container.style.opacity)) {
      container.style.opacity = opacity === 1 ? '' : opacity;
    }

    return this.container;
  }

  /**
   * @inheritDoc
   */
  forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, callback, declutteredFeatures) {
    if (!this.replayGroup_) {
      return undefined;
    } else {
      const resolution = frameState.viewState.resolution;
      const rotation = frameState.viewState.rotation;
      const layer = /** @type {import("../../layer/Vector").default} */ (this.getLayer());
      /** @type {!Object<string, boolean>} */
      const features = {};
      const result = this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution, rotation, hitTolerance, {},
        /**
         * @param {import("../../Feature.js").FeatureLike} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          const key = getUid(feature);
          if (!(key in features)) {
            features[key] = true;
            return callback(feature, layer);
          }
        }, layer.getDeclutter() ? declutteredFeatures : null);
      return result;
    }
  }

  /**
   * @inheritDoc
   */
  handleFontsChanged() {
    const layer = this.getLayer();
    if (layer.getVisible() && this.replayGroup_) {
      layer.changed();
    }
  }

  /**
   * Handle changes in image style state.
   * @param {import("../../events/Event.js").default} event Image style change event.
   * @private
   */
  handleStyleImageChange_(event) {
    this.renderIfReadyAndVisible();
  }

  /**
   * @inheritDoc
   */
  prepareFrame(frameState) {
    const vectorLayer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer());
    const vectorSource = vectorLayer.getSource();

    const animating = frameState.viewHints[ViewHint.ANIMATING];
    const interacting = frameState.viewHints[ViewHint.INTERACTING];
    const updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
    const updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();

    if (!this.dirty_ && (!updateWhileAnimating && animating) ||
        (!updateWhileInteracting && interacting)) {
      return true;
    }

    const frameStateExtent = frameState.extent;
    const viewState = frameState.viewState;
    const projection = viewState.projection;
    const resolution = viewState.resolution;
    const pixelRatio = frameState.pixelRatio;
    const vectorLayerRevision = vectorLayer.getRevision();
    const vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
    let vectorLayerRenderOrder = vectorLayer.getRenderOrder();

    if (vectorLayerRenderOrder === undefined) {
      vectorLayerRenderOrder = defaultRenderOrder;
    }

    const extent = buffer(frameStateExtent,
      vectorLayerRenderBuffer * resolution);
    const projectionExtent = viewState.projection.getExtent();

    if (vectorSource.getWrapX() && viewState.projection.canWrapX() &&
        !containsExtent(projectionExtent, frameState.extent)) {
      // For the replay group, we need an extent that intersects the real world
      // (-180° to +180°). To support geometries in a coordinate range from -540°
      // to +540°, we add at least 1 world width on each side of the projection
      // extent. If the viewport is wider than the world, we need to add half of
      // the viewport width to make sure we cover the whole viewport.
      const worldWidth = getWidth(projectionExtent);
      const gutter = Math.max(getWidth(extent) / 2, worldWidth);
      extent[0] = projectionExtent[0] - gutter;
      extent[2] = projectionExtent[2] + gutter;
    }

    if (!this.dirty_ &&
        this.renderedResolution_ == resolution &&
        this.renderedRevision_ == vectorLayerRevision &&
        this.renderedRenderOrder_ == vectorLayerRenderOrder &&
        containsExtent(this.renderedExtent_, extent)) {
      this.replayGroupChanged = false;
      return true;
    }

    if (this.replayGroup_) {
      this.replayGroup_.dispose();
    }
    this.replayGroup_ = null;

    this.dirty_ = false;

    const replayGroup = new CanvasBuilderGroup(
      getRenderTolerance(resolution, pixelRatio), extent, resolution,
      pixelRatio, vectorLayer.getDeclutter());

    vectorSource.loadFeatures(extent, resolution, projection);

    /**
     * @param {import("../../Feature.js").default} feature Feature.
     * @this {CanvasVectorLayerRenderer}
     */
    const render = function(feature) {
      let styles;
      const styleFunction = feature.getStyleFunction() || vectorLayer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resolution);
      }
      if (styles) {
        const dirty = this.renderFeature(
          feature, resolution, pixelRatio, styles, replayGroup);
        this.dirty_ = this.dirty_ || dirty;
      }
    }.bind(this);
    if (vectorLayerRenderOrder) {
      /** @type {Array<import("../../Feature.js").default>} */
      const features = [];
      vectorSource.forEachFeatureInExtent(extent,
        /**
         * @param {import("../../Feature.js").default} feature Feature.
         */
        function(feature) {
          features.push(feature);
        });
      features.sort(vectorLayerRenderOrder);
      for (let i = 0, ii = features.length; i < ii; ++i) {
        render(features[i]);
      }
    } else {
      vectorSource.forEachFeatureInExtent(extent, render);
    }

    const replayGroupInstructions = replayGroup.finish();
    const executorGroup = new ExecutorGroup(extent, resolution,
      pixelRatio, vectorSource.getOverlaps(),
      replayGroupInstructions, vectorLayer.getRenderBuffer());

    this.renderedResolution_ = resolution;
    this.renderedRevision_ = vectorLayerRevision;
    this.renderedRenderOrder_ = vectorLayerRenderOrder;
    this.renderedExtent_ = extent;
    this.replayGroup_ = executorGroup;

    this.replayGroupChanged = true;
    return true;
  }

  /**
   * @param {import("../../Feature.js").default} feature Feature.
   * @param {number} resolution Resolution.
   * @param {number} pixelRatio Pixel ratio.
   * @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
   * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group.
   * @return {boolean} `true` if an image is loading.
   */
  renderFeature(feature, resolution, pixelRatio, styles, builderGroup) {
    if (!styles) {
      return false;
    }
    let loading = false;
    if (Array.isArray(styles)) {
      const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio);
      for (let i = 0, ii = styles.length; i < ii; ++i) {
        loading = renderFeature(
          builderGroup, feature, styles[i], squaredTolerance,
          this.handleStyleImageChange_, this) || loading;
      }
    } else {
      loading = renderFeature(
        builderGroup, feature, styles,
        getSquaredRenderTolerance(resolution, pixelRatio),
        this.handleStyleImageChange_, this);
    }
    return loading;
  }
}

export default CanvasVectorLayerRenderer;
<|endoftext|>


<commit_before>var Config = require('Config');

// Locale variabelen.
var currentUserLocation
  , mapView
  , annotationsCount;
var annotationsArray = [];

/**
 * Stelt de reden in waarom de app de huidige locatie van de gebruiker wil
 * opvragen.
 */
Titanium.Geolocation.purpose = Config.GelocationIntent;

/**
 * Stelt een locale referentie in naar de mapview die in de app wordt gebruikt.
 *
 * De rest van de module is onbruikbaar als setMapView niet voor de tijd een
 * keer correct aangeroepen is.
 *
 * @param {Titanium.Map.View} [mapview]
 * 		Een Titaniium.Map.View die door de module moet worden gebruikt.
 */
var setMapView = function(mapview) {
	mapView = mapview;
};

/**
 * Wrapper functie om de mapview.setLocation functie. Aan de hand van een
 * lat/long coordinaat en een zoomniveau word de map naar de opgegeven
 * locatie geanimeerd.
 *
 * @param {number} [latitude]
 * 		De latitude van de locatie.
 * @param {number} [longitude]
 * 		De longitude van de locatie.
 * @param {number} [zoom]
 * 		Het niveau van inzoomen in de vorm van lat/longDelta
 *
 */
var setLocation = function(latitude, longitude, zoom) {
	mapView.setLocation({
		latitude: 		latitude,
		longitude: 		longitude,
		latitudeDelta: 	zoom,
		longitudeDelta: zoom,
		animate:		true
	});
};

/**
 * Ververst de huidige locatie van de gebruiker.
 */
var updateGeolocation = function() {
	if (Ti.Geolocation.locationServicesEnabled) {
		Titanium.Geolocation.getCurrentPosition(function(e) {
	        if (e.error) {
	            Ti.API.error('Error: ' + e.error);
	        } else {
	            currentUserLocation = e.coords;
	        }
	    });
	} else {
		alert(Config.NoGeolocationMsg);
	}
};

/**
 * Geeft de huidige locatie van de gebruiker als object.
 *
 * @returns {Object}
 * 		De huidige locatie van de gebruiker als object met lat/long.
 */
var getUserLocation = function() {
	if (!currentUserLocation) {
		updateGeolocation;
	}

	return currentUserLocation;
};

/**
 * Een wrapper om de orginele addAnnotation functie.
 *
 * @param {Object} [data]
 * 		Het data object dat de gegevens bevat van de annotatie.
 *
 * @param {string} [icon]
 * 		Een string die de locatie van het icoontje dat de annotatie moet
 * 		gebruiken aangeeft.
 *
 * @param {string} [subtitle]
 * 		De ondertitel van de annotatie.
 */
var addAnnotation = function(data, icon, subtitle) {
	var newAnnotation;

	// Check of de minimaal benodigde data om een annotatie te maken bestaat.
	if (!(data && data.LAT && data.LON && data.title)) {
		return;
	}

	newAnnotation = Titanium.Map.createAnnotation({
		animate:	true,

		dataToPass: data,

		latitude: 	data.LAT,
		longitude: 	data.LON,

		title:		data.title,
		subtitle: 	subtitle,

		pincolor:	Titanium.Map.ANNOTATION_GREEN,
		image: 		icon,

		rightButton: Titanium.Platform.osname === 'android' ?
			Config.AndroidrightButton : Titanium.UI.iPhone.SystemButton.DISCLOSURE
	});

	mapView.addAnnotation(newAnnotation);
};

/**
 *	Alle annotations toevoegen aan de map uit de meegegeven array en met de
 * 		juiste iconen (Gemaakt voor de bruggen)
 * 	@param {array} [dataArray]
 * 		De array aan data dat dient te worden toegevoegd
 *
 * 	@param {string} [iconGreen]
 * 		Het incoontje dat gebruikt dient te worden wanneer we er onderdoor
 * 			te kunnen.
 * 	@param {string} [iconRed]
 * 		Het incoontje dat gebruikt dient te worden wanneer we er NIET onderdoor
 * 			kunnen.
 */
var annotationsArray = function(dataArray, iconGreen, iconRed){
	// Kijken of er een hoogte ingeven is
	var height = parseFloat(Titanium.App.Properties.getString('height', null));
	// Loop door alle data
	for(var i = 0; i < dataArray.length; i++)
	{
		// Subtitel opbouwen uit de hoogte en breedte
		var subtitle = Config.AnnotationSubHeight + dataArray[i].HEIGTH + '\t'
				+ Config.AnnotationSubWidth + dataArray[i].WIDTH;
		// Kijken of we er onderdoor kunnen
		if(!height || dataArray[i].HEIGTH > height ) {
			addAnnotation(dataArray[i], iconGreen, subtitle );
		}
		// en anders
		else{
			addAnnotation(dataArray[i], iconRed, subtitle );
		}
	}
};

/**
 *	Een methode die de te tonen annotaties gaat filteren op afstand
 *
 * 	@param {Array} [annotationsData]
 * 		An array contatianing the needed annotations
 * 	@param {Object} [region]
 * 		Een map regio waar we ons nu bevinden
 * 	@param {string} [iconGreen]
 * 		Link naar de afbeelding die we moeten inladen bij normale weergave of
 * 			wanneer we er wel onderdoor kunnen
 * 	@param {string} [iconRed]
 * 		Link naar de afbeelding die we moeten inladen wanneer we er niet onderdoor kunne
 */
var filterAnnotations = function(annotationsData, region, iconGreen, iconRed){
	// Controleren of we een regio meegekregen hebben
	if(!region || !annotationsData || !iconGreen) {
		Titanium.API.error('Meegegeven data komt niet overeen. Error vanuit filterAnnotations functie!');
		return;
	}
	// Kijken of we niet al te ver uitgezoomed zijn
	if((region.latitudeDelta > 0.1 && region.longitudeDelta  > 0.05 ) ||
		(region.longitudeDelta > 0.1 && region.latitudeDelta > 0.05)){
		// indien dan alle punten verwijderen
		mapView.removeAllAnnotations();
		// en einde functie
		return;
	}

	/*
	 * REGION EIGENSCHAPPEN
	 *
	 * Properties:
	 *	latitude : Number
	 *		Latitude value for the center point of the map, in decimal degrees.
	 *	latitudeDelta : Number
	 *		The amount of north-to-south distance displayed on the map, measured in decimal degrees.
	 *	longitude : Number
	 *		Longitude value for the center point of the map, in decimal degrees.
	 *	longitudeDelta : Number
	 *		The amount of east-to-west distance displayed on the map, measured in decimal degrees.
	 */

	// Maak twee variabelen die we nodig hebben aan
	var toAddAnnotations = 	[];
	var counter = 			0;
	// Opgeslagen hoogte inlezen
	var height = parseFloat(Titanium.App.Properties.getString('height', null));
	// Coordinaten berekenen (lat en long worden vanuit het midden meegegeven)
	// De boven waarden
	var top	=		region.latitude - (region.latitudeDelta / 2);
	var bottom = 	region.latitude + (region.latitudeDelta / 2);
	// En de onder waarden
	var left = 		region.longitude - (region.longitudeDelta / 2);
	var right = 	region.longitude + (region.longitudeDelta / 2);

	Titanium.API.warn('top: ' + top + ' Bottom: ' + bottom + ' left: ' + left + ' right: ' +  right);
	// Kijken welke we dienen toe te voegen aan de array
	for(var i = 0; i < annotationsData.length; i++){
		// Kijken of het binnen ons bereik ligt
		if(annotationsData[i].LAT > top && annotationsData[i].LAT < bottom &&
			annotationsData[i].LON > left && annotationsData[i].LON < right){
			// indoen we voldoen het onderschrift maken
			var subtitle = '';
			if(annotationsData[i].HEIGHT){
				subtitle += Config.AnnotationSubHeight + annotationsData[i].HEIGHT + '\t';
			}
			if(annotationsData[i].WIDTH){
				subtitle += Config.AnnotationSubWidth + annotationsData[i].WIDTH;
			}
			// Maak een annotatie (Hij valt binnen de waarden dus zou op de kaart moeten komen)
			toAddAnnotations[counter] = Titanium.Map.createAnnotation({
				// Voor sneller laden
				animate:	false,

				// Custom property voor het maken van een
				// detail pagina
				dataToPass: annotationsData[i],

				latitude: 	annotationsData[i].LAT,
				longitude: 	annotationsData[i].LON,

				title:		annotationsData[i].title,
				subtitle: 	subtitle,

				pincolor:	Titanium.Map.ANNOTATION_GREEN,
				// Kijken welke afbeelding we moeten invullen
				image: 		(annotationsData[i].HEIGHT && height && iconRed && annotationsData[i].HEIGHT < height) ?
								iconRed : iconGreen,

				rightButton: Titanium.Platform.osname === 'android' ?
					Config.AndroidrightButton : Titanium.UI.iPhone.SystemButton.DISCLOSURE
			});

			// 1 aangemaakt teller verhogen
			counter++;
		}	// Bereik controle
	}

	// Kijken hoeveel we er nu hebben
	//annotationsCount = counter + annotationsCount;

	// Indien dit meer is dan toegestaan de kaart leeg gooien
	//if(annotationsCount > Config.maxAnnotations){
	//	// Punten verwijderen
	//	removeAnnotations(annotationsArray);
	//	// En de counter opnieuw instellen
	//	annotationsArray = [];
	//	annotationsCount = counter;
	//}
	// For testing :D
	removeAnnotations(annotationsArray);

	Titanium.API.warn('Aantal: ' + counter + ' uit de mogelijke ' + annotationsData.length);
	// Voeg alle annotaties toe aan de kaart
	annotationsArray = toAddAnnotations;
	mapView.addAnnotations(toAddAnnotations);
};

var concat = function(destination, source){
	for(var i = 0; i < source.length; i++){
		destination[destination.length] = source[i];
	}
	return destination
};

var removeAnnotations = function(toRemove){
	Titanium.API.warn('Annotaties op de kaart: ' + toRemove.length);
	for(var i = 0; i < toRemove.length; i++){
		mapView.removeAnnotation(toRemove[i]);
	}
	annotationsArray = [];
};

/**
 * 	De onderstaande functie zal annotaions toevoegen op de kaart om zo een trail
 * 		te maken.
 * 	Indien er al x aantal annotaions op de kaart staan zal hij de als eerste
 * 		toegevoegde annotation weghalen.
 *
 * 	@param {int} [plaats]
 * 		De postite om op te beginnen met loggen
 */
var trailers = [];
function showTrail(plaats){
	// Controle of we niet al teveel annotaions op de kaart hebben
	// Als we al we te veel hebben zetten we de pointer op 0 (en dan opnieuw eroverheen)
	if(plaats > Config.AmountOfTrail) { plaats = 0; }

	// Kijken of we een positie kunnen krijg
	if(Titanium.Geolocation.getLocationServicesEnabled()){
		// Locatie updaten
		updateGeolocation();
		// En uitlezen
		var location = getUserLocation();
		// Kijken of dit iets terug gaf en of we bewegen
		if(location && location.speed && location.speed > 0)
		{
			// Indien dan zal er een nieuwe annotaion gemaakt worden maar eerst zullen we een oude annotation verwijderen
			if(trailers[plaats])
			{
				mapView.removeAnnotation(trailers[plaats]);
			}
			// Darna maken we een nieuw annotion aan op deze lokatie in de array
			trailers[plaats] = Titanium.Map.createAnnotation({
				latitude:	location.latitude,
				longitude:	location.longitude,
				title:		'',
				opacity: 	1,
				duration: 	3000,
				image: '/images/trailstip.png'
			});
			// Daarna deze annotatie toeveogen aan de kaart
			mapView.addAnnotation(trailers[plaats]);
		}
	}
	// Toevoegen en verwijderen is klaar
	// Functie opnieuw aanroepen na een timout (en pointer met 1 verhogen)
	setTimeout(function(){
 		showTrail(plaats + 1)}, Config.TrailerTimeout);
}

// Maak een aantal functies van de module publiek beschikbaar.
exports.setMapView = 		setMapView;
exports.setLocation = 		setLocation;
exports.updateGeolocation = updateGeolocation;
exports.getUserLocation = 	getUserLocation;
exports.addAnnotation =		addAnnotation;
exports.annotationsArray = 	annotationsArray;
exports.showTrail = 		showTrail;
exports.filterAnnotations = filterAnnotations;
//exports.removeAnnotations = removeAnnotations;
<commit_msg>Tada marten!<commit_after>var Config = require('Config');

// Locale variabelen.
var currentUserLocation
  , mapView
  , annotationsCount;
var annotationsArray = [];

/**
 * Stelt de reden in waarom de app de huidige locatie van de gebruiker wil
 * opvragen.
 */
Titanium.Geolocation.purpose = Config.GelocationIntent;

/**
 * Stelt een locale referentie in naar de mapview die in de app wordt gebruikt.
 *
 * De rest van de module is onbruikbaar als setMapView niet voor de tijd een
 * keer correct aangeroepen is.
 *
 * @param {Titanium.Map.View} [mapview]
 * 		Een Titaniium.Map.View die door de module moet worden gebruikt.
 */
var setMapView = function(mapview) {
	mapView = mapview;
};

/**
 * Wrapper functie om de mapview.setLocation functie. Aan de hand van een
 * lat/long coordinaat en een zoomniveau word de map naar de opgegeven
 * locatie geanimeerd.
 *
 * @param {number} [latitude]
 * 		De latitude van de locatie.
 * @param {number} [longitude]
 * 		De longitude van de locatie.
 * @param {number} [zoom]
 * 		Het niveau van inzoomen in de vorm van lat/longDelta
 *
 */
var setLocation = function(latitude, longitude, zoom) {
	mapView.setLocation({
		latitude: 		latitude,
		longitude: 		longitude,
		latitudeDelta: 	zoom,
		longitudeDelta: zoom,
		animate:		true
	});
};

/**
 * Ververst de huidige locatie van de gebruiker.
 */
var updateGeolocation = function() {
	if (Ti.Geolocation.locationServicesEnabled) {
		Titanium.Geolocation.getCurrentPosition(function(e) {
	        if (e.error) {
	            Ti.API.error('Error: ' + e.error);
	        } else {
	            currentUserLocation = e.coords;
	        }
	    });
	} else {
		alert(Config.NoGeolocationMsg);
	}
};

/**
 * Geeft de huidige locatie van de gebruiker als object.
 *
 * @returns {Object}
 * 		De huidige locatie van de gebruiker als object met lat/long.
 */
var getUserLocation = function() {
	if (!currentUserLocation) {
		updateGeolocation;
	}

	return currentUserLocation;
};

/**
 * Een wrapper om de orginele addAnnotation functie.
 *
 * @param {Object} [data]
 * 		Het data object dat de gegevens bevat van de annotatie.
 *
 * @param {string} [icon]
 * 		Een string die de locatie van het icoontje dat de annotatie moet
 * 		gebruiken aangeeft.
 *
 * @param {string} [subtitle]
 * 		De ondertitel van de annotatie.
 */
var addAnnotation = function(data, icon, subtitle) {
	var newAnnotation;

	// Check of de minimaal benodigde data om een annotatie te maken bestaat.
	if (!(data && data.LAT && data.LON && data.title)) {
		return;
	}

	newAnnotation = Titanium.Map.createAnnotation({
		animate:	true,

		dataToPass: data,

		latitude: 	data.LAT,
		longitude: 	data.LON,

		title:		data.title,
		subtitle: 	subtitle,

		pincolor:	Titanium.Map.ANNOTATION_GREEN,
		image: 		icon,

		rightButton: Titanium.Platform.osname === 'android' ?
			Config.AndroidrightButton : Titanium.UI.iPhone.SystemButton.DISCLOSURE
	});

	mapView.addAnnotation(newAnnotation);
};

/**
 *	Alle annotations toevoegen aan de map uit de meegegeven array en met de
 * 		juiste iconen (Gemaakt voor de bruggen)
 * 	@param {array} [dataArray]
 * 		De array aan data dat dient te worden toegevoegd
 *
 * 	@param {string} [iconGreen]
 * 		Het incoontje dat gebruikt dient te worden wanneer we er onderdoor
 * 			te kunnen.
 * 	@param {string} [iconRed]
 * 		Het incoontje dat gebruikt dient te worden wanneer we er NIET onderdoor
 * 			kunnen.
 */
var annotationsArray = function(dataArray, iconGreen, iconRed){
	// Kijken of er een hoogte ingeven is
	var height = parseFloat(Titanium.App.Properties.getString('height', null));
	// Loop door alle data
	for(var i = 0; i < dataArray.length; i++)
	{
		// Subtitel opbouwen uit de hoogte en breedte
		var subtitle = Config.AnnotationSubHeight + dataArray[i].HEIGTH + '\t'
				+ Config.AnnotationSubWidth + dataArray[i].WIDTH;
		// Kijken of we er onderdoor kunnen
		if(!height || dataArray[i].HEIGTH > height ) {
			addAnnotation(dataArray[i], iconGreen, subtitle );
		}
		// en anders
		else{
			addAnnotation(dataArray[i], iconRed, subtitle );
		}
	}
};

/**
 *	Een methode die de te tonen annotaties gaat filteren op afstand
 *
 * 	@param {Array} [annotationsData]
 * 		An array contatianing the needed annotations
 * 	@param {Object} [region]
 * 		Een map regio waar we ons nu bevinden
 * 	@param {string} [iconGreen]
 * 		Link naar de afbeelding die we moeten inladen bij normale weergave of
 * 			wanneer we er wel onderdoor kunnen
 * 	@param {string} [iconRed]
 * 		Link naar de afbeelding die we moeten inladen wanneer we er niet onderdoor kunne
 */
var filterAnnotations = function(annotationsData, region, iconGreen, iconRed){
	// Controleren of we een regio meegekregen hebben
	if(!region || !annotationsData || !iconGreen) {
		Titanium.API.error('Meegegeven data komt niet overeen. Error vanuit filterAnnotations functie!');
		return;
	}
	// Kijken of we niet al te ver uitgezoomed zijn
	if((region.latitudeDelta > 0.1 && region.longitudeDelta  > 0.05 ) ||
		(region.longitudeDelta > 0.1 && region.latitudeDelta > 0.05)){
		// indien dan alle punten verwijderen
		mapView.removeAllAnnotations();
		// en einde functie
		return;
	}

	/*
	 * REGION EIGENSCHAPPEN
	 *
	 * Properties:
	 *	latitude : Number
	 *		Latitude value for the center point of the map, in decimal degrees.
	 *	latitudeDelta : Number
	 *		The amount of north-to-south distance displayed on the map, measured in decimal degrees.
	 *	longitude : Number
	 *		Longitude value for the center point of the map, in decimal degrees.
	 *	longitudeDelta : Number
	 *		The amount of east-to-west distance displayed on the map, measured in decimal degrees.
	 */

	// Maak twee variabelen die we nodig hebben aan
	var toAddAnnotations = 	[];
	var counter = 			0;
	// Opgeslagen hoogte inlezen
	var height = parseFloat(Titanium.App.Properties.getString('height', null));
	// Coordinaten berekenen (lat en long worden vanuit het midden meegegeven)
	// De boven waarden
	var top	=		region.latitude - (region.latitudeDelta / 2);
	var bottom = 	region.latitude + (region.latitudeDelta / 2);
	// En de onder waarden
	var left = 		region.longitude - (region.longitudeDelta / 2);
	var right = 	region.longitude + (region.longitudeDelta / 2);

	Titanium.API.warn('top: ' + top + ' Bottom: ' + bottom + ' left: ' + left + ' right: ' +  right);
	// Kijken welke we dienen toe te voegen aan de array
	for(var i = 0; i < annotationsData.length; i++){
		// Kijken of het binnen ons bereik ligt
		if(annotationsData[i].LAT > top && annotationsData[i].LAT < bottom &&
			annotationsData[i].LON > left && annotationsData[i].LON < right){
			// indoen we voldoen het onderschrift maken
			var subtitle = '';
			if(annotationsData[i].HEIGHT){
				subtitle += Config.AnnotationSubHeight + annotationsData[i].HEIGHT + '\t';
			}
			if(annotationsData[i].WIDTH){
				subtitle += Config.AnnotationSubWidth + annotationsData[i].WIDTH;
			}
			// Maak een annotatie (Hij valt binnen de waarden dus zou op de kaart moeten komen)
			toAddAnnotations[counter] = Titanium.Map.createAnnotation({
				// Voor sneller laden
				animate:	false,

				// Custom property voor het maken van een
				// detail pagina
				dataToPass: annotationsData[i],

				latitude: 	annotationsData[i].LAT,
				longitude: 	annotationsData[i].LON,

				title:		annotationsData[i].title,
				subtitle: 	subtitle,

				pincolor:	Titanium.Map.ANNOTATION_GREEN,
				// Kijken welke afbeelding we moeten invullen
				image: 		(annotationsData[i].HEIGHT && height && iconRed && annotationsData[i].HEIGHT < height) ?
								iconRed : iconGreen,

				rightButton: Titanium.Platform.osname === 'android' ?
					Config.AndroidrightButton : Titanium.UI.iPhone.SystemButton.DISCLOSURE
			});

			// 1 aangemaakt teller verhogen
			counter++;
		}	// Bereik controle
	}

	// Kijken hoeveel we er nu hebben
	//annotationsCount = counter + annotationsCount;

	// Indien dit meer is dan toegestaan de kaart leeg gooien
	//if(annotationsCount > Config.maxAnnotations){
	//	// Punten verwijderen
	//	removeAnnotations(annotationsArray);
	//	// En de counter opnieuw instellen
	//	annotationsArray = [];
	//	annotationsCount = counter;
	//}
	// For testing :D
	//removeAnnotations(annotationsArray);
	mapView.removeAllAnnotations();

	Titanium.API.warn('Aantal: ' + counter + ' uit de mogelijke ' + annotationsData.length);
	// Voeg alle annotaties toe aan de kaart
	annotationsArray = toAddAnnotations;
	mapView.addAnnotations(toAddAnnotations);
	mapView.addAnnotations(trailers);
};

var concat = function(destination, source){
	for(var i = 0; i < source.length; i++){
		destination[destination.length] = source[i];
	}
	return destination
};

var removeAnnotations = function(toRemove){
	Titanium.API.warn('Annotaties op de kaart: ' + toRemove.length);
	for(var i = 0; i < toRemove.length; i++){
		mapView.removeAnnotation(toRemove[i]);
	}
	annotationsArray = [];
};

/**
 * 	De onderstaande functie zal annotaions toevoegen op de kaart om zo een trail
 * 		te maken.
 * 	Indien er al x aantal annotaions op de kaart staan zal hij de als eerste
 * 		toegevoegde annotation weghalen.
 *
 * 	@param {int} [plaats]
 * 		De postite om op te beginnen met loggen
 */
var trailers = [];
function showTrail(plaats){
	// Controle of we niet al teveel annotaions op de kaart hebben
	// Als we al we te veel hebben zetten we de pointer op 0 (en dan opnieuw eroverheen)
	if(plaats > Config.AmountOfTrail) { plaats = 0; }

	// Kijken of we een positie kunnen krijg
	if(Titanium.Geolocation.getLocationServicesEnabled()){
		// Locatie updaten
		updateGeolocation();
		// En uitlezen
		var location = getUserLocation();
		// Kijken of dit iets terug gaf en of we bewegen
		if(location && location.speed && location.speed > 0)
		{
			// Indien dan zal er een nieuwe annotaion gemaakt worden maar eerst zullen we een oude annotation verwijderen
			if(trailers[plaats])
			{
				mapView.removeAnnotation(trailers[plaats]);
			}
			// Darna maken we een nieuw annotion aan op deze lokatie in de array
			trailers[plaats] = Titanium.Map.createAnnotation({
				latitude:	location.latitude,
				longitude:	location.longitude,
				title:		'',
				opacity: 	1,
				duration: 	3000,
				image: '/images/trailstip.png'
			});
			// Daarna deze annotatie toeveogen aan de kaart
			mapView.addAnnotation(trailers[plaats]);
		}
	}
	// Toevoegen en verwijderen is klaar
	// Functie opnieuw aanroepen na een timout (en pointer met 1 verhogen)
	setTimeout(function(){
 		showTrail(plaats + 1)}, Config.TrailerTimeout);
}

// Maak een aantal functies van de module publiek beschikbaar.
exports.setMapView = 		setMapView;
exports.setLocation = 		setLocation;
exports.updateGeolocation = updateGeolocation;
exports.getUserLocation = 	getUserLocation;
exports.addAnnotation =		addAnnotation;
exports.annotationsArray = 	annotationsArray;
exports.showTrail = 		showTrail;
exports.filterAnnotations = filterAnnotations;
//exports.removeAnnotations = removeAnnotations;
<|endoftext|>


<commit_before>'use strict';

var extend = require('extend'),
    mkdirp = require('mkdirp'),
    path = require('path'),
    glob = require('glob');

function beforeInstall(config, utils, next) {
  utils.log('► Add models dependencies... ');

  config.package = extend(true, {
    dependencies: {
      async: '^0.9.2',
      'events-manager': '^1.0.4'
    }
  }, config.package);

  if (config.install.mongodb) {
    config.package = extend(true, {
      dependencies: {
        mongoose: '^4.0.6'
      }
    }, config.package);
  }

  if (config.install.sql) {
    config.package = extend(true, {
      dependencies: {
        sequelize: '^3.3.0'
      }
    }, config.package);
  }

  if (config.install.mysql) {
    config.package = extend(true, {
      dependencies: {
        mysql: '^2.7.0'
      }
    }, config.package);
  }

  utils.log('[OK]\n');

  next();
}

function afterInstall(config, utils, next) {
  var source = path.resolve(__dirname, 'resources'),
      featuresDest = path.resolve(utils.path, 'features');

  utils.log('► Create common models files... ');

  mkdirp.sync(path.join(featuresDest, 'common/models'));
  mkdirp.sync(path.join(featuresDest, 'favicon/models'));
  mkdirp.sync(path.join(featuresDest, 'shortcuts/models'));

  utils.copy(path.join(source, 'common-models-gulpfile.js'), path.join(featuresDest, 'common/common-models-gulpfile.js'));
  utils.copy(path.join(source, 'favicon-service.js'), path.join(featuresDest, 'favicon/models/favicon-service.js'));
  utils.copy(path.join(source, 'shortcuts-service.js'), path.join(featuresDest, 'shortcuts/models/shortcuts-service.js'));

  glob.sync(path.join(source, 'common-*-@(factory|model|service).js')).forEach(function(file) {
    var filename = file.split('/').pop();
    utils.copy(file, path.join(featuresDest, 'common/models', filename));
  });

  utils.log('[OK]\n');

  next();
}

module.exports = {
  install: [{
    type: 'confirm',
    name: 'mongodb',
    message: 'Use MongoDB connector (Mongoose)?',
    default: true
  }, {
    type: 'confirm',
    name: 'sql',
    message: 'Use SQL connector (Sequelize)?',
    default: true
  }, {
    type: 'confirm',
    name: 'mysql',
    message: 'Use MySQL (with Sequelize)?',
    default: true,
    when: function(answers) {
      return answers.sql;
    }
  }],
  beforeInstall: beforeInstall,
  afterInstall: afterInstall,
  env: [{
    type: 'input',
    name: 'MONGO_COUNT',
    message: 'How many MongoDB databases you want to use?'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_NAME',
    message: 'Name your MongoDB instance {{index1}} (capitalized):'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_HOST',
    message: 'MongoDB instance {{index1}} host (with port):'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_USER',
    message: 'MongoDB instance {{index1}} user:'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_PASSWORD',
    message: 'MongoDB instance {{index1}} password:'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_DB',
    message: 'MongoDB instance {{index1}} database name:'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_POOL_SIZE',
    message: 'MongoDB instance {{index1}} pool size:',
    default: 19
  }, {
    type: 'input',
    name: 'SQL_COUNT',
    message: 'How many SQL databases you want to use?'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_NAME',
    message: 'Name your SQL instance {{index1}} (capitalized):'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_TYPE',
    message: 'Type of your instance {{index1}} (mysql|mariadb|sqlite|postgres|mssql):'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_HOST',
    message: 'SQL instance {{index1}} host (with port):'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_USER',
    message: 'SQL instance {{index1}} user:'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_PASSWORD',
    message: 'SQL instance {{index1}} password:'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_DB',
    message: 'SQL instance {{index1}} database name:'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_POOL_SIZE',
    message: 'SQL instance {{index1}} pool size:',
    default: 5
  }]
};
<commit_msg>fix(models cli): copy common-async-gulpfile.js and common-events-manager-gulpfile.js files<commit_after>'use strict';

var extend = require('extend'),
    mkdirp = require('mkdirp'),
    path = require('path'),
    glob = require('glob');

function beforeInstall(config, utils, next) {
  utils.log('► Add models dependencies... ');

  config.package = extend(true, {
    dependencies: {
      async: '^0.9.2',
      'events-manager': '^1.0.4'
    }
  }, config.package);

  if (config.install.mongodb) {
    config.package = extend(true, {
      dependencies: {
        mongoose: '^4.0.6'
      }
    }, config.package);
  }

  if (config.install.sql) {
    config.package = extend(true, {
      dependencies: {
        sequelize: '^3.3.0'
      }
    }, config.package);
  }

  if (config.install.mysql) {
    config.package = extend(true, {
      dependencies: {
        mysql: '^2.7.0'
      }
    }, config.package);
  }

  utils.log('[OK]\n');

  next();
}

function afterInstall(config, utils, next) {
  var source = path.resolve(__dirname, 'resources'),
      featuresDest = path.resolve(utils.path, 'features');

  utils.log('► Create common models files... ');

  mkdirp.sync(path.join(featuresDest, 'common/models'));
  mkdirp.sync(path.join(featuresDest, 'favicon/models'));
  mkdirp.sync(path.join(featuresDest, 'shortcuts/models'));

  utils.copy(path.join(source, 'common-models-gulpfile.js'), path.join(featuresDest, 'common/common-models-gulpfile.js'));
  utils.copy(path.join(source, 'common-async-gulpfile.js'), path.join(featuresDest, 'common/common-async-gulpfile.js'));
  utils.copy(path.join(source, 'common-events-manager-gulpfile.js'), path.join(featuresDest, 'common/common-events-manager-gulpfile.js'));
  utils.copy(path.join(source, 'favicon-service.js'), path.join(featuresDest, 'favicon/models/favicon-service.js'));
  utils.copy(path.join(source, 'shortcuts-service.js'), path.join(featuresDest, 'shortcuts/models/shortcuts-service.js'));

  glob.sync(path.join(source, 'common-*-@(factory|model|service).js')).forEach(function(file) {
    var filename = file.split('/').pop();
    utils.copy(file, path.join(featuresDest, 'common/models', filename));
  });

  utils.log('[OK]\n');

  next();
}

module.exports = {
  install: [{
    type: 'confirm',
    name: 'mongodb',
    message: 'Use MongoDB connector (Mongoose)?',
    default: true
  }, {
    type: 'confirm',
    name: 'sql',
    message: 'Use SQL connector (Sequelize)?',
    default: true
  }, {
    type: 'confirm',
    name: 'mysql',
    message: 'Use MySQL (with Sequelize)?',
    default: true,
    when: function(answers) {
      return answers.sql;
    }
  }],
  beforeInstall: beforeInstall,
  afterInstall: afterInstall,
  env: [{
    type: 'input',
    name: 'MONGO_COUNT',
    message: 'How many MongoDB databases you want to use?'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_NAME',
    message: 'Name your MongoDB instance {{index1}} (capitalized):'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_HOST',
    message: 'MongoDB instance {{index1}} host (with port):'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_USER',
    message: 'MongoDB instance {{index1}} user:'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_PASSWORD',
    message: 'MongoDB instance {{index1}} password:'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_DB',
    message: 'MongoDB instance {{index1}} database name:'
  }, {
    loop: 'MONGO_COUNT',
    type: 'input',
    name: 'MONGO_POOL_SIZE',
    message: 'MongoDB instance {{index1}} pool size:',
    default: 19
  }, {
    type: 'input',
    name: 'SQL_COUNT',
    message: 'How many SQL databases you want to use?'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_NAME',
    message: 'Name your SQL instance {{index1}} (capitalized):'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_TYPE',
    message: 'Type of your instance {{index1}} (mysql|mariadb|sqlite|postgres|mssql):'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_HOST',
    message: 'SQL instance {{index1}} host (with port):'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_USER',
    message: 'SQL instance {{index1}} user:'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_PASSWORD',
    message: 'SQL instance {{index1}} password:'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_DB',
    message: 'SQL instance {{index1}} database name:'
  }, {
    loop: 'SQL_COUNT',
    type: 'input',
    name: 'SQL_POOL_SIZE',
    message: 'SQL instance {{index1}} pool size:',
    default: 5
  }]
};
<|endoftext|>


<commit_before>(function() {
    'use strict';

    angular
        .module('antidote')
        .controller('DrugsListCtrl', DrugsListCtrl)
        .controller('DrugsViewCtrl', DrugsViewCtrl)
        .controller('DrugsOverviewCtrl', DrugsOverviewCtrl)
        .controller('DrugsReviewsCtrl', DrugsReviewsCtrl)
        .controller('DrugsAlternativesCtrl', DrugsAlternativesCtrl)
        .controller('DrugsReviewCtrl', DrugsReviewCtrl)
        .controller('DrugsReviewModalCtrl', DrugsReviewModalCtrl);

    /** @ngInject */
    function DrugsListCtrl(DrugsService, $stateParams) {
        var that = this;
        this.letters = [
            'a', 'b', 'c', 'd', 'e', 'f', 'g',
            'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
            'q', 'r', 's',
            't', 'u', 'v',
            'w', 'x', 'y',
            'z'
        ];

        activate();

        function activate() {
            DrugsService.query($stateParams).$promise.then(function(drugs) {
                that.drugs = drugs;
            });
        }

        this.getAlphabetFilterClass = function(alphabet) {
            if ($stateParams.alpha === alphabet) {
                return 'active';
            } else {
                return '';
            }
        };
        this.getAlphabetLetter = function() {
            return $stateParams.alpha;
        };
    }

    /** @ngInject */
    function DrugsViewCtrl(DrugsService, $stateParams, $mdDialog) {
        var that = this;
        activate();

        this.tabs = [
            {title: 'Overview', state: 'drugs.view.overview'},
            {title: 'Reviews', state: 'drugs.view.reviews'},
            {title: 'Alternatives', state: 'drugs.view.alternatives'}
        ];

        this.openReviewModal = function(ev) {
            $mdDialog.show({
                controller: 'DrugsReviewModalCtrl',
                controllerAs: 'drugsReviewModal',
                templateUrl: '/app/drugs/drugs.review.modal.html',
                targetEvent: ev,
                clickOutsideToClose: true,
                hasBackdrop: true
            });
        };

        function activate() {
            DrugsService.get({id: $stateParams.id}).$promise.then(function(drug) {
                that.drug = drug;
            });
        }
    }

    /** @ngInject */
    function DrugsReviewModalCtrl($mdDialog) {
        this.closeDialog = function() {
            $mdDialog.hide();
        };
    }

    /** controller for review form **/
    /** @ngInject */
    function DrugsReviewCtrl(DrugsService, $stateParams) {
        var that = this;
        this.review = {};
        this.reviewSubmitted = false;

        this.submitReview = function() {
            DrugsService.postReview({id: $stateParams.id}, this.review).$promise.then(function() {
                that.reviewSubmitted = true;
            });
            //TODO: add server error handling to display messages to user
        };
    }

    /** @ngInject */
    function DrugsOverviewCtrl() {
        activate();

        this.labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
        this.series = ['Series A', 'Series B'];
        this.data = [
            [65, 59, 80, 81, 56, 55, 40],
            [28, 48, 40, 19, 86, 27, 90]
        ];

        function activate() {
            //TODO: add API service call here
        }
    }

    /** @ngInject */
    function DrugsReviewsCtrl(DrugsService, $stateParams) {
        this.reviews = {};
        var that = this;

        activate();

        function activate() {
            DrugsService.getReviews({id: $stateParams.id}).$promise.then(function(reviews) {
                that.reviews = reviews.data;
            });
        }
    }

    /** @ngInject */
    function DrugsAlternativesCtrl(DrugsService, $stateParams) {
        this.alternatives = {};
        var that = this;

        activate();

        function activate() {
            DrugsService.getAlternatives({id: $stateParams.id}).$promise.then(function(alternatives) {
                that.alternatives = alternatives;
            });
        }
    }
})();
<commit_msg>fix review modal template path<commit_after>(function() {
    'use strict';

    angular
        .module('antidote')
        .controller('DrugsListCtrl', DrugsListCtrl)
        .controller('DrugsViewCtrl', DrugsViewCtrl)
        .controller('DrugsOverviewCtrl', DrugsOverviewCtrl)
        .controller('DrugsReviewsCtrl', DrugsReviewsCtrl)
        .controller('DrugsAlternativesCtrl', DrugsAlternativesCtrl)
        .controller('DrugsReviewCtrl', DrugsReviewCtrl)
        .controller('DrugsReviewModalCtrl', DrugsReviewModalCtrl);

    /** @ngInject */
    function DrugsListCtrl(DrugsService, $stateParams) {
        var that = this;
        this.letters = [
            'a', 'b', 'c', 'd', 'e', 'f', 'g',
            'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
            'q', 'r', 's',
            't', 'u', 'v',
            'w', 'x', 'y',
            'z'
        ];

        activate();

        function activate() {
            DrugsService.query($stateParams).$promise.then(function(drugs) {
                that.drugs = drugs;
            });
        }

        this.getAlphabetFilterClass = function(alphabet) {
            if ($stateParams.alpha === alphabet) {
                return 'active';
            } else {
                return '';
            }
        };
        this.getAlphabetLetter = function() {
            return $stateParams.alpha;
        };
    }

    /** @ngInject */
    function DrugsViewCtrl(DrugsService, $stateParams, $mdDialog) {
        var that = this;
        activate();

        this.tabs = [
            {title: 'Overview', state: 'drugs.view.overview'},
            {title: 'Reviews', state: 'drugs.view.reviews'},
            {title: 'Alternatives', state: 'drugs.view.alternatives'}
        ];

        this.openReviewModal = function(ev) {
            $mdDialog.show({
                controller: 'DrugsReviewModalCtrl',
                controllerAs: 'drugsReviewModal',
                templateUrl: 'app/drugs/drugs.review.modal.html',
                targetEvent: ev,
                clickOutsideToClose: true,
                hasBackdrop: true
            });
        };

        function activate() {
            DrugsService.get({id: $stateParams.id}).$promise.then(function(drug) {
                that.drug = drug;
            });
        }
    }

    /** @ngInject */
    function DrugsReviewModalCtrl($mdDialog) {
        this.closeDialog = function() {
            $mdDialog.hide();
        };
    }

    /** controller for review form **/
    /** @ngInject */
    function DrugsReviewCtrl(DrugsService, $stateParams) {
        var that = this;
        this.review = {};
        this.reviewSubmitted = false;

        this.submitReview = function() {
            DrugsService.postReview({id: $stateParams.id}, this.review).$promise.then(function() {
                that.reviewSubmitted = true;
            });
            //TODO: add server error handling to display messages to user
        };
    }

    /** @ngInject */
    function DrugsOverviewCtrl() {
        activate();

        this.labels = ['January', 'February', 'March', 'April', 'May', 'June', 'July'];
        this.series = ['Series A', 'Series B'];
        this.data = [
            [65, 59, 80, 81, 56, 55, 40],
            [28, 48, 40, 19, 86, 27, 90]
        ];

        function activate() {
            //TODO: add API service call here
        }
    }

    /** @ngInject */
    function DrugsReviewsCtrl(DrugsService, $stateParams) {
        this.reviews = {};
        var that = this;

        activate();

        function activate() {
            DrugsService.getReviews({id: $stateParams.id}).$promise.then(function(reviews) {
                that.reviews = reviews.data;
            });
        }
    }

    /** @ngInject */
    function DrugsAlternativesCtrl(DrugsService, $stateParams) {
        this.alternatives = {};
        var that = this;

        activate();

        function activate() {
            DrugsService.getAlternatives({id: $stateParams.id}).$promise.then(function(alternatives) {
                that.alternatives = alternatives;
            });
        }
    }
})();
<|endoftext|>


<commit_before><commit_msg>Clarify error statement<commit_after><|endoftext|>


<commit_before>/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import AutocompleteWrapperModel from "./autocomplete";
import Avatar from "../Avatar";

class BasePart {
    constructor(text = "") {
        this._text = text;
    }

    acceptsInsertion(chr) {
        return true;
    }

    acceptsRemoval(position, chr) {
        return true;
    }

    merge(part) {
        return false;
    }

    split(offset) {
        const splitText = this.text.substr(offset);
        this._text = this.text.substr(0, offset);
        return new PlainPart(splitText);
    }

    // removes len chars, or returns the plain text this part should be replaced with
    // if the part would become invalid if it removed everything.
    remove(offset, len) {
        // validate
        const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len);
        for (let i = offset; i < (len + offset); ++i) {
            const chr = this.text.charAt(i);
            if (!this.acceptsRemoval(i, chr)) {
                return strWithRemoval;
            }
        }
        this._text = strWithRemoval;
    }

    // append str, returns the remaining string if a character was rejected.
    appendUntilRejected(str) {
        for (let i = 0; i < str.length; ++i) {
            const chr = str.charAt(i);
            if (!this.acceptsInsertion(chr, i)) {
                this._text = this._text + str.substr(0, i);
                return str.substr(i);
            }
        }
        this._text = this._text + str;
    }

    // inserts str at offset if all the characters in str were accepted, otherwise don't do anything
    // return whether the str was accepted or not.
    insertAll(offset, str) {
        for (let i = 0; i < str.length; ++i) {
            const chr = str.charAt(i);
            if (!this.acceptsInsertion(chr)) {
                return false;
            }
        }
        const beforeInsert = this._text.substr(0, offset);
        const afterInsert = this._text.substr(offset);
        this._text = beforeInsert + str + afterInsert;
        return true;
    }

    createAutoComplete() {}

    trim(len) {
        const remaining = this._text.substr(len);
        this._text = this._text.substr(0, len);
        return remaining;
    }

    get text() {
        return this._text;
    }

    get canEdit() {
        return true;
    }

    toString() {
        return `${this.type}(${this.text})`;
    }

    serialize() {
        return {type: this.type, text: this.text};
    }
}

class PlainPart extends BasePart {
    acceptsInsertion(chr) {
        return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
    }

    toDOMNode() {
        return document.createTextNode(this.text);
    }

    merge(part) {
        if (part.type === this.type) {
            this._text = this.text + part.text;
            return true;
        }
        return false;
    }

    get type() {
        return "plain";
    }

    updateDOMNode(node) {
        if (node.textContent !== this.text) {
            // console.log("changing plain text from", node.textContent, "to", this.text);
            node.textContent = this.text;
        }
    }

    canUpdateDOMNode(node) {
        return node.nodeType === Node.TEXT_NODE;
    }
}

class PillPart extends BasePart {
    constructor(resourceId, label) {
        super(label);
        this.resourceId = resourceId;
    }

    acceptsInsertion(chr) {
        return chr !== " ";
    }

    acceptsRemoval(position, chr) {
        return position !== 0;  //if you remove initial # or @, pill should become plain
    }

    toDOMNode() {
        const container = document.createElement("span");
        container.className = this.className;
        container.appendChild(document.createTextNode(this.text));
        this.setAvatar(container);
        return container;
    }

    updateDOMNode(node) {
        const textNode = node.childNodes[0];
        if (textNode.textContent !== this.text) {
            textNode.textContent = this.text;
        }
        if (node.className !== this.className) {
            node.className = this.className;
        }
        this.setAvatar(node);
    }

    canUpdateDOMNode(node) {
        return node.nodeType === Node.ELEMENT_NODE &&
               node.nodeName === "SPAN" &&
               node.childNodes.length === 1 &&
               node.childNodes[0].nodeType === Node.TEXT_NODE;
    }

    // helper method for subclasses
    _setAvatarVars(node, avatarUrl, initialLetter) {
        const avatarBackground = `url('${avatarUrl}')`;
        const avatarLetter = `'${initialLetter}'`;
        // check if the value is changing,
        // otherwise the avatars flicker on every keystroke while updating.
        if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
            node.style.setProperty("--avatar-background", avatarBackground);
        }
        if (node.style.getPropertyValue("--avatar-letter") !== avatarLetter) {
            node.style.setProperty("--avatar-letter", avatarLetter);
        }
    }

    get canEdit() {
        return false;
    }
}

class NewlinePart extends BasePart {
    acceptsInsertion(chr, i) {
        return (this.text.length + i) === 0 && chr === "\n";
    }

    acceptsRemoval(position, chr) {
        return true;
    }

    toDOMNode() {
        return document.createElement("br");
    }

    merge() {
        return false;
    }

    updateDOMNode() {}

    canUpdateDOMNode(node) {
        return node.tagName === "BR";
    }

    get type() {
        return "newline";
    }

    // this makes the cursor skip this part when it is inserted
    // rather than trying to append to it, which is what we want.
    // As a newline can also be only one character, it makes sense
    // as it can only be one character long. This caused #9741.
    get canEdit() {
        return false;
    }
}

class RoomPillPart extends PillPart {
    constructor(displayAlias, room) {
        super(displayAlias, displayAlias);
        this._room = room;
    }

    setAvatar(node) {
        let initialLetter = "";
        let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio);
        if (!avatarUrl) {
            initialLetter = Avatar.getInitialLetter(this._room.name);
            avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room.roomId)}`;
        }
        this._setAvatarVars(node, avatarUrl, initialLetter);
    }

    get type() {
        return "room-pill";
    }

    get className() {
        return "mx_RoomPill mx_Pill";
    }
}

class UserPillPart extends PillPart {
    constructor(userId, displayName, member) {
        super(userId, displayName);
        this._member = member;
    }

    setAvatar(node) {
        const name = this._member.name || this._member.userId;
        const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId);
        let avatarUrl = Avatar.avatarUrlForMember(
            this._member,
            16 * window.devicePixelRatio,
            16 * window.devicePixelRatio);
        let initialLetter = "";
        if (avatarUrl === defaultAvatarUrl) {
            // the url from defaultAvatarUrlForString is meant to go in an img element,
            // which has the base of the document. we're using it in css,
            // which has the base of the theme css file, two levels deeper than the document,
            // so go up to the level of the document.
            avatarUrl = `../../${avatarUrl}`;
            initialLetter = Avatar.getInitialLetter(name);
        }
        this._setAvatarVars(node, avatarUrl, initialLetter);
    }

    get type() {
        return "user-pill";
    }

    get className() {
        return "mx_UserPill mx_Pill";
    }

    serialize() {
        const obj = super.serialize();
        obj.userId = this.resourceId;
        return obj;
    }
}

class PillCandidatePart extends PlainPart {
    constructor(text, autoCompleteCreator) {
        super(text);
        this._autoCompleteCreator = autoCompleteCreator;
    }

    createAutoComplete(updateCallback) {
        return this._autoCompleteCreator(updateCallback);
    }

    acceptsInsertion(chr, i) {
        if ((this.text.length + i) === 0) {
            return true;
        } else {
            return super.acceptsInsertion(chr, i);
        }
    }

    merge() {
        return false;
    }

    acceptsRemoval(position, chr) {
        return true;
    }

    get type() {
        return "pill-candidate";
    }
}

export class PartCreator {
    constructor(getAutocompleterComponent, updateQuery, room, client) {
        this._room = room;
        this._client = client;
        this._autoCompleteCreator = (updateCallback) => {
            return new AutocompleteWrapperModel(
                updateCallback,
                getAutocompleterComponent,
                updateQuery,
                this,
            );
        };
    }

    createPartForInput(input) {
        switch (input[0]) {
            case "#":
            case "@":
            case ":":
                return this.pillCandidate("");
            case "\n":
                return new NewlinePart();
            default:
                return new PlainPart();
        }
    }

    createDefaultPart(text) {
        return this.plain(text);
    }

    deserializePart(part) {
        switch (part.type) {
            case "plain":
                return this.plain(part.text);
            case "newline":
                return this.newline();
            case "pill-candidate":
                return this.pillCandidate(part.text);
            case "room-pill":
                return this.roomPill(part.text);
            case "user-pill":
                return this.userPill(part.text, part.userId);
        }
    }

    plain(text) {
        return new PlainPart(text);
    }

    newline() {
        return new NewlinePart("\n");
    }

    pillCandidate(text) {
        return new PillCandidatePart(text, this._autoCompleteCreator);
    }

    roomPill(alias) {
        let room;
        if (alias[0] === '#') {
            room = this._client.getRooms().find((r) => {
                return r.getAliases().includes(alias);
            });
        } else {
            room = this._client.getRoom(alias);
        }
        return new RoomPillPart(alias, room);
    }

    userPill(displayName, userId) {
        const member = this._room.getMember(userId);
        return new UserPillPart(userId, displayName, member);
    }
}

<commit_msg>support creating @room pills in partcreator<commit_after>/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

import AutocompleteWrapperModel from "./autocomplete";
import Avatar from "../Avatar";

class BasePart {
    constructor(text = "") {
        this._text = text;
    }

    acceptsInsertion(chr) {
        return true;
    }

    acceptsRemoval(position, chr) {
        return true;
    }

    merge(part) {
        return false;
    }

    split(offset) {
        const splitText = this.text.substr(offset);
        this._text = this.text.substr(0, offset);
        return new PlainPart(splitText);
    }

    // removes len chars, or returns the plain text this part should be replaced with
    // if the part would become invalid if it removed everything.
    remove(offset, len) {
        // validate
        const strWithRemoval = this.text.substr(0, offset) + this.text.substr(offset + len);
        for (let i = offset; i < (len + offset); ++i) {
            const chr = this.text.charAt(i);
            if (!this.acceptsRemoval(i, chr)) {
                return strWithRemoval;
            }
        }
        this._text = strWithRemoval;
    }

    // append str, returns the remaining string if a character was rejected.
    appendUntilRejected(str) {
        for (let i = 0; i < str.length; ++i) {
            const chr = str.charAt(i);
            if (!this.acceptsInsertion(chr, i)) {
                this._text = this._text + str.substr(0, i);
                return str.substr(i);
            }
        }
        this._text = this._text + str;
    }

    // inserts str at offset if all the characters in str were accepted, otherwise don't do anything
    // return whether the str was accepted or not.
    insertAll(offset, str) {
        for (let i = 0; i < str.length; ++i) {
            const chr = str.charAt(i);
            if (!this.acceptsInsertion(chr)) {
                return false;
            }
        }
        const beforeInsert = this._text.substr(0, offset);
        const afterInsert = this._text.substr(offset);
        this._text = beforeInsert + str + afterInsert;
        return true;
    }

    createAutoComplete() {}

    trim(len) {
        const remaining = this._text.substr(len);
        this._text = this._text.substr(0, len);
        return remaining;
    }

    get text() {
        return this._text;
    }

    get canEdit() {
        return true;
    }

    toString() {
        return `${this.type}(${this.text})`;
    }

    serialize() {
        return {type: this.type, text: this.text};
    }
}

class PlainPart extends BasePart {
    acceptsInsertion(chr) {
        return chr !== "@" && chr !== "#" && chr !== ":" && chr !== "\n";
    }

    toDOMNode() {
        return document.createTextNode(this.text);
    }

    merge(part) {
        if (part.type === this.type) {
            this._text = this.text + part.text;
            return true;
        }
        return false;
    }

    get type() {
        return "plain";
    }

    updateDOMNode(node) {
        if (node.textContent !== this.text) {
            // console.log("changing plain text from", node.textContent, "to", this.text);
            node.textContent = this.text;
        }
    }

    canUpdateDOMNode(node) {
        return node.nodeType === Node.TEXT_NODE;
    }
}

class PillPart extends BasePart {
    constructor(resourceId, label) {
        super(label);
        this.resourceId = resourceId;
    }

    acceptsInsertion(chr) {
        return chr !== " ";
    }

    acceptsRemoval(position, chr) {
        return position !== 0;  //if you remove initial # or @, pill should become plain
    }

    toDOMNode() {
        const container = document.createElement("span");
        container.className = this.className;
        container.appendChild(document.createTextNode(this.text));
        this.setAvatar(container);
        return container;
    }

    updateDOMNode(node) {
        const textNode = node.childNodes[0];
        if (textNode.textContent !== this.text) {
            textNode.textContent = this.text;
        }
        if (node.className !== this.className) {
            node.className = this.className;
        }
        this.setAvatar(node);
    }

    canUpdateDOMNode(node) {
        return node.nodeType === Node.ELEMENT_NODE &&
               node.nodeName === "SPAN" &&
               node.childNodes.length === 1 &&
               node.childNodes[0].nodeType === Node.TEXT_NODE;
    }

    // helper method for subclasses
    _setAvatarVars(node, avatarUrl, initialLetter) {
        const avatarBackground = `url('${avatarUrl}')`;
        const avatarLetter = `'${initialLetter}'`;
        // check if the value is changing,
        // otherwise the avatars flicker on every keystroke while updating.
        if (node.style.getPropertyValue("--avatar-background") !== avatarBackground) {
            node.style.setProperty("--avatar-background", avatarBackground);
        }
        if (node.style.getPropertyValue("--avatar-letter") !== avatarLetter) {
            node.style.setProperty("--avatar-letter", avatarLetter);
        }
    }

    get canEdit() {
        return false;
    }
}

class NewlinePart extends BasePart {
    acceptsInsertion(chr, i) {
        return (this.text.length + i) === 0 && chr === "\n";
    }

    acceptsRemoval(position, chr) {
        return true;
    }

    toDOMNode() {
        return document.createElement("br");
    }

    merge() {
        return false;
    }

    updateDOMNode() {}

    canUpdateDOMNode(node) {
        return node.tagName === "BR";
    }

    get type() {
        return "newline";
    }

    // this makes the cursor skip this part when it is inserted
    // rather than trying to append to it, which is what we want.
    // As a newline can also be only one character, it makes sense
    // as it can only be one character long. This caused #9741.
    get canEdit() {
        return false;
    }
}

class RoomPillPart extends PillPart {
    constructor(displayAlias, room) {
        super(displayAlias, displayAlias);
        this._room = room;
    }

    setAvatar(node) {
        let initialLetter = "";
        let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio);
        if (!avatarUrl) {
            initialLetter = Avatar.getInitialLetter(this._room.name);
            avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room.roomId)}`;
        }
        this._setAvatarVars(node, avatarUrl, initialLetter);
    }

    get type() {
        return "room-pill";
    }

    get className() {
        return "mx_RoomPill mx_Pill";
    }
}

class AtRoomPillPart extends RoomPillPart {
    get type() {
        return "at-room-pill";
    }
}

class UserPillPart extends PillPart {
    constructor(userId, displayName, member) {
        super(userId, displayName);
        this._member = member;
    }

    setAvatar(node) {
        const name = this._member.name || this._member.userId;
        const defaultAvatarUrl = Avatar.defaultAvatarUrlForString(this._member.userId);
        let avatarUrl = Avatar.avatarUrlForMember(
            this._member,
            16 * window.devicePixelRatio,
            16 * window.devicePixelRatio);
        let initialLetter = "";
        if (avatarUrl === defaultAvatarUrl) {
            // the url from defaultAvatarUrlForString is meant to go in an img element,
            // which has the base of the document. we're using it in css,
            // which has the base of the theme css file, two levels deeper than the document,
            // so go up to the level of the document.
            avatarUrl = `../../${avatarUrl}`;
            initialLetter = Avatar.getInitialLetter(name);
        }
        this._setAvatarVars(node, avatarUrl, initialLetter);
    }

    get type() {
        return "user-pill";
    }

    get className() {
        return "mx_UserPill mx_Pill";
    }

    serialize() {
        const obj = super.serialize();
        obj.userId = this.resourceId;
        return obj;
    }
}

class PillCandidatePart extends PlainPart {
    constructor(text, autoCompleteCreator) {
        super(text);
        this._autoCompleteCreator = autoCompleteCreator;
    }

    createAutoComplete(updateCallback) {
        return this._autoCompleteCreator(updateCallback);
    }

    acceptsInsertion(chr, i) {
        if ((this.text.length + i) === 0) {
            return true;
        } else {
            return super.acceptsInsertion(chr, i);
        }
    }

    merge() {
        return false;
    }

    acceptsRemoval(position, chr) {
        return true;
    }

    get type() {
        return "pill-candidate";
    }
}

export class PartCreator {
    constructor(getAutocompleterComponent, updateQuery, room, client) {
        this._room = room;
        this._client = client;
        this._autoCompleteCreator = (updateCallback) => {
            return new AutocompleteWrapperModel(
                updateCallback,
                getAutocompleterComponent,
                updateQuery,
                this,
            );
        };
    }

    createPartForInput(input) {
        switch (input[0]) {
            case "#":
            case "@":
            case ":":
                return this.pillCandidate("");
            case "\n":
                return new NewlinePart();
            default:
                return new PlainPart();
        }
    }

    createDefaultPart(text) {
        return this.plain(text);
    }

    deserializePart(part) {
        switch (part.type) {
            case "plain":
                return this.plain(part.text);
            case "newline":
                return this.newline();
            case "pill-candidate":
                return this.pillCandidate(part.text);
            case "room-pill":
                return this.roomPill(part.text);
            case "user-pill":
                return this.userPill(part.text, part.userId);
        }
    }

    plain(text) {
        return new PlainPart(text);
    }

    newline() {
        return new NewlinePart("\n");
    }

    pillCandidate(text) {
        return new PillCandidatePart(text, this._autoCompleteCreator);
    }

    roomPill(alias) {
        let room;
        if (alias[0] === '#') {
            room = this._client.getRooms().find((r) => {
                return r.getAliases().includes(alias);
            });
        } else {
            room = this._client.getRoom(alias);
        }
        return new RoomPillPart(alias, room);
    }

    atRoomPill(text) {
        return new AtRoomPillPart(text, this._room);
    }

    userPill(displayName, userId) {
        const member = this._room.getMember(userId);
        return new UserPillPart(userId, displayName, member);
    }
}

<|endoftext|>