<commit_before>goog.provide('ol.format.TopoJSON');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.JSONFeature');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');

/**
 * @classdesc
 * Feature format for reading and writing data in the TopoJSON format.
 *
 * @constructor
 * @extends {ol.format.JSONFeature}
 * @param {olx.format.TopoJSONOptions=} opt_options Options.
 * @api stable
 */
ol.format.TopoJSON = function(opt_options) {

  var options = goog.isDef(opt_options) ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get(
      goog.isDefAndNotNull(options.defaultDataProjection) ?
          options.defaultDataProjection : 'EPSG:4326');

};
goog.inherits(ol.format.TopoJSON, ol.format.JSONFeature);

/**
 * @const {Array.<string>}
 * @private
 */
ol.format.TopoJSON.EXTENSIONS_ = ['.topojson'];

/**
 * Concatenate arcs into a coordinate array.
 * @param {Array.<number>} indices Indices of arcs to concatenate.  Negative
 *     values indicate arcs need to be reversed.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs (already
 *     transformed).
 * @return {Array.<ol.Coordinate>} Coordinates array.
 * @private
 */
ol.format.TopoJSON.concatenateArcs_ = function(indices, arcs) {
  /** @type {Array.<ol.Coordinate>} */
  var coordinates = [];
  var index, arc;
  var i, ii;
  var j, jj;
  for (i = 0, ii = indices.length; i < ii; ++i) {
    index = indices[i];
    if (i > 0) {
      // splicing together arcs, discard last point
      coordinates.pop();
    }
    if (index >= 0) {
      // forward arc
      arc = arcs[index];
    } else {
      // reverse arc
      arc = arcs[~index].slice().reverse();
    }
    coordinates.push.apply(coordinates, arc);
  }
  // provide fresh copies of coordinate arrays
  for (j = 0, jj = coordinates.length; j < jj; ++j) {
    coordinates[j] = coordinates[j].slice();
  }
  return coordinates;
};

/**
 * Create a point from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @return {ol.geom.Point} Geometry.
 * @private
 */
ol.format.TopoJSON.readPointGeometry_ = function(object, scale, translate) {
  var coordinates = object.coordinates;
  if (!goog.isNull(scale) && !goog.isNull(translate)) {
    ol.format.TopoJSON.transformVertex_(coordinates, scale, translate);
  }
  return new ol.geom.Point(coordinates);
};

/**
 * Create a multi-point from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @return {ol.geom.MultiPoint} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiPointGeometry_ = function(object, scale,
    translate) {
  var coordinates = object.coordinates;
  var i, ii;
  if (!goog.isNull(scale) && !goog.isNull(translate)) {
    for (i = 0, ii = coordinates.length; i < ii; ++i) {
      ol.format.TopoJSON.transformVertex_(coordinates[i], scale, translate);
    }
  }
  return new ol.geom.MultiPoint(coordinates);
};

/**
 * Create a linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.LineString} Geometry.
 * @private
 */
ol.format.TopoJSON.readLineStringGeometry_ = function(object, arcs) {
  var coordinates = ol.format.TopoJSON.concatenateArcs_(object.arcs, arcs);
  return new ol.geom.LineString(coordinates);
};

/**
 * Create a multi-linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.MultiLineString} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiLineStringGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new ol.geom.MultiLineString(coordinates);
};

/**
 * Create a polygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.Polygon} Geometry.
 * @private
 */
ol.format.TopoJSON.readPolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new ol.geom.Polygon(coordinates);
};

/**
 * Create a multi-polygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.MultiPolygon} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiPolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var polyArray, ringCoords, j, jj;
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    // for each polygon
    polyArray = object.arcs[i];
    ringCoords = [];
    for (j = 0, jj = polyArray.length; j < jj; ++j) {
      // for each ring
      ringCoords[j] = ol.format.TopoJSON.concatenateArcs_(polyArray[j], arcs);
    }
    coordinates[i] = ringCoords;
  }
  return new ol.geom.MultiPolygon(coordinates);
};

/**
 * @inheritDoc
 */
ol.format.TopoJSON.prototype.getExtensions = function() {
  return ol.format.TopoJSON.EXTENSIONS_;
};

/**
 * Create features from a TopoJSON GeometryCollection object.
 *
 * @param {TopoJSONGeometryCollection} collection TopoJSON Geometry
 *     object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.format.TopoJSON.readFeaturesFromGeometryCollection_ = function(
    collection, arcs, scale, translate, opt_options) {
  var geometries = collection.geometries;
  var features = [];
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    features[i] = ol.format.TopoJSON.readFeatureFromGeometry_(
        geometries[i], arcs, scale, translate, opt_options);
  }
  return features;
};

/**
 * Create a feature from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON geometry object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @private
 */
ol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs,
    scale, translate, opt_options) {
  var geometry;
  var type = object.type;
  var geometryReader = ol.format.TopoJSON.GEOMETRY_READERS_[type];
  goog.asserts.assert(goog.isDef(geometryReader));
  if ((type === 'Point') || (type === 'MultiPoint')) {
    geometry = geometryReader(object, scale, translate);
  } else {
    geometry = geometryReader(object, arcs);
  }
  var feature = new ol.Feature();
  feature.setGeometry(/** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(geometry, false, opt_options)));
  if (goog.isDef(object.id)) {
    feature.setId(object.id);
  }
  if (goog.isDef(object.properties)) {
    feature.setProperties(object.properties);
  }
  return feature;
};

/**
 * Read all features from a TopoJSON source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.TopoJSON.prototype.readFeatures;

/**
 * @inheritDoc
 */
ol.format.TopoJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  if (object.type == 'Topology') {
    var topoJSONTopology = /** @type {TopoJSONTopology} */ (object);
    var transform, scale = null, translate = null;
    if (goog.isDef(topoJSONTopology.transform)) {
      transform = /** @type {TopoJSONTransform} */
          (topoJSONTopology.transform);
      scale = transform.scale;
      translate = transform.translate;
    }
    var arcs = topoJSONTopology.arcs;
    if (goog.isDef(transform)) {
      ol.format.TopoJSON.transformArcs_(arcs, scale, translate);
    }
    /** @type {Array.<ol.Feature>} */
    var features = [];
    var topoJSONFeatures = goog.object.getValues(topoJSONTopology.objects);
    var i, ii;
    var feature;
    for (i = 0, ii = topoJSONFeatures.length; i < ii; ++i) {
      if (topoJSONFeatures[i].type === 'GeometryCollection') {
        feature = /** @type {TopoJSONGeometryCollection} */
            (topoJSONFeatures[i]);
        features.push.apply(features,
            ol.format.TopoJSON.readFeaturesFromGeometryCollection_(
                feature, arcs, scale, translate, opt_options));
      } else {
        feature = /** @type {TopoJSONGeometry} */
            (topoJSONFeatures[i]);
        features.push(ol.format.TopoJSON.readFeatureFromGeometry_(
            feature, arcs, scale, translate, opt_options));
      }
    }
    return features;
  } else {
    return [];
  }
};

/**
 * Apply a linear transform to array of arcs.  The provided array of arcs is
 * modified in place.
 *
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformArcs_ = function(arcs, scale, translate) {
  var i, ii;
  for (i = 0, ii = arcs.length; i < ii; ++i) {
    ol.format.TopoJSON.transformArc_(arcs[i], scale, translate);
  }
};

/**
 * Apply a linear transform to an arc.  The provided arc is modified in place.
 *
 * @param {Array.<ol.Coordinate>} arc Arc.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformArc_ = function(arc, scale, translate) {
  var x = 0;
  var y = 0;
  var vertex;
  var i, ii;
  for (i = 0, ii = arc.length; i < ii; ++i) {
    vertex = arc[i];
    x += vertex[0];
    y += vertex[1];
    vertex[0] = x;
    vertex[1] = y;
    ol.format.TopoJSON.transformVertex_(vertex, scale, translate);
  }
};

/**
 * Apply a linear transform to a vertex.  The provided vertex is modified in
 * place.
 *
 * @param {ol.Coordinate} vertex Vertex.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformVertex_ = function(vertex, scale, translate) {
  vertex[0] = vertex[0] * scale[0] + translate[0];
  vertex[1] = vertex[1] * scale[1] + translate[1];
};

/**
 * Read the projection from a TopoJSON source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} object Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.TopoJSON.prototype.readProjection = function(object) {
  return this.defaultDataProjection;
};

/**
 * @const
 * @private
 * @type {Object.<string, function(TopoJSONGeometry, Array, ...[Array]): ol.geom.Geometry>}
 */
ol.format.TopoJSON.GEOMETRY_READERS_ = {
  'Point': ol.format.TopoJSON.readPointGeometry_,
  'LineString': ol.format.TopoJSON.readLineStringGeometry_,
  'Polygon': ol.format.TopoJSON.readPolygonGeometry_,
  'MultiPoint': ol.format.TopoJSON.readMultiPointGeometry_,
  'MultiLineString': ol.format.TopoJSON.readMultiLineStringGeometry_,
  'MultiPolygon': ol.format.TopoJSON.readMultiPolygonGeometry_
};
<commit_msg>Remove square brackets from type<commit_after>goog.provide('ol.format.TopoJSON');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.JSONFeature');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');

/**
 * @classdesc
 * Feature format for reading and writing data in the TopoJSON format.
 *
 * @constructor
 * @extends {ol.format.JSONFeature}
 * @param {olx.format.TopoJSONOptions=} opt_options Options.
 * @api stable
 */
ol.format.TopoJSON = function(opt_options) {

  var options = goog.isDef(opt_options) ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get(
      goog.isDefAndNotNull(options.defaultDataProjection) ?
          options.defaultDataProjection : 'EPSG:4326');

};
goog.inherits(ol.format.TopoJSON, ol.format.JSONFeature);

/**
 * @const {Array.<string>}
 * @private
 */
ol.format.TopoJSON.EXTENSIONS_ = ['.topojson'];

/**
 * Concatenate arcs into a coordinate array.
 * @param {Array.<number>} indices Indices of arcs to concatenate.  Negative
 *     values indicate arcs need to be reversed.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs (already
 *     transformed).
 * @return {Array.<ol.Coordinate>} Coordinates array.
 * @private
 */
ol.format.TopoJSON.concatenateArcs_ = function(indices, arcs) {
  /** @type {Array.<ol.Coordinate>} */
  var coordinates = [];
  var index, arc;
  var i, ii;
  var j, jj;
  for (i = 0, ii = indices.length; i < ii; ++i) {
    index = indices[i];
    if (i > 0) {
      // splicing together arcs, discard last point
      coordinates.pop();
    }
    if (index >= 0) {
      // forward arc
      arc = arcs[index];
    } else {
      // reverse arc
      arc = arcs[~index].slice().reverse();
    }
    coordinates.push.apply(coordinates, arc);
  }
  // provide fresh copies of coordinate arrays
  for (j = 0, jj = coordinates.length; j < jj; ++j) {
    coordinates[j] = coordinates[j].slice();
  }
  return coordinates;
};

/**
 * Create a point from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @return {ol.geom.Point} Geometry.
 * @private
 */
ol.format.TopoJSON.readPointGeometry_ = function(object, scale, translate) {
  var coordinates = object.coordinates;
  if (!goog.isNull(scale) && !goog.isNull(translate)) {
    ol.format.TopoJSON.transformVertex_(coordinates, scale, translate);
  }
  return new ol.geom.Point(coordinates);
};

/**
 * Create a multi-point from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @return {ol.geom.MultiPoint} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiPointGeometry_ = function(object, scale,
    translate) {
  var coordinates = object.coordinates;
  var i, ii;
  if (!goog.isNull(scale) && !goog.isNull(translate)) {
    for (i = 0, ii = coordinates.length; i < ii; ++i) {
      ol.format.TopoJSON.transformVertex_(coordinates[i], scale, translate);
    }
  }
  return new ol.geom.MultiPoint(coordinates);
};

/**
 * Create a linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.LineString} Geometry.
 * @private
 */
ol.format.TopoJSON.readLineStringGeometry_ = function(object, arcs) {
  var coordinates = ol.format.TopoJSON.concatenateArcs_(object.arcs, arcs);
  return new ol.geom.LineString(coordinates);
};

/**
 * Create a multi-linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.MultiLineString} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiLineStringGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new ol.geom.MultiLineString(coordinates);
};

/**
 * Create a polygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.Polygon} Geometry.
 * @private
 */
ol.format.TopoJSON.readPolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new ol.geom.Polygon(coordinates);
};

/**
 * Create a multi-polygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.MultiPolygon} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiPolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var polyArray, ringCoords, j, jj;
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    // for each polygon
    polyArray = object.arcs[i];
    ringCoords = [];
    for (j = 0, jj = polyArray.length; j < jj; ++j) {
      // for each ring
      ringCoords[j] = ol.format.TopoJSON.concatenateArcs_(polyArray[j], arcs);
    }
    coordinates[i] = ringCoords;
  }
  return new ol.geom.MultiPolygon(coordinates);
};

/**
 * @inheritDoc
 */
ol.format.TopoJSON.prototype.getExtensions = function() {
  return ol.format.TopoJSON.EXTENSIONS_;
};

/**
 * Create features from a TopoJSON GeometryCollection object.
 *
 * @param {TopoJSONGeometryCollection} collection TopoJSON Geometry
 *     object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.format.TopoJSON.readFeaturesFromGeometryCollection_ = function(
    collection, arcs, scale, translate, opt_options) {
  var geometries = collection.geometries;
  var features = [];
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    features[i] = ol.format.TopoJSON.readFeatureFromGeometry_(
        geometries[i], arcs, scale, translate, opt_options);
  }
  return features;
};

/**
 * Create a feature from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON geometry object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @private
 */
ol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs,
    scale, translate, opt_options) {
  var geometry;
  var type = object.type;
  var geometryReader = ol.format.TopoJSON.GEOMETRY_READERS_[type];
  goog.asserts.assert(goog.isDef(geometryReader));
  if ((type === 'Point') || (type === 'MultiPoint')) {
    geometry = geometryReader(object, scale, translate);
  } else {
    geometry = geometryReader(object, arcs);
  }
  var feature = new ol.Feature();
  feature.setGeometry(/** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(geometry, false, opt_options)));
  if (goog.isDef(object.id)) {
    feature.setId(object.id);
  }
  if (goog.isDef(object.properties)) {
    feature.setProperties(object.properties);
  }
  return feature;
};

/**
 * Read all features from a TopoJSON source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.TopoJSON.prototype.readFeatures;

/**
 * @inheritDoc
 */
ol.format.TopoJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  if (object.type == 'Topology') {
    var topoJSONTopology = /** @type {TopoJSONTopology} */ (object);
    var transform, scale = null, translate = null;
    if (goog.isDef(topoJSONTopology.transform)) {
      transform = /** @type {TopoJSONTransform} */
          (topoJSONTopology.transform);
      scale = transform.scale;
      translate = transform.translate;
    }
    var arcs = topoJSONTopology.arcs;
    if (goog.isDef(transform)) {
      ol.format.TopoJSON.transformArcs_(arcs, scale, translate);
    }
    /** @type {Array.<ol.Feature>} */
    var features = [];
    var topoJSONFeatures = goog.object.getValues(topoJSONTopology.objects);
    var i, ii;
    var feature;
    for (i = 0, ii = topoJSONFeatures.length; i < ii; ++i) {
      if (topoJSONFeatures[i].type === 'GeometryCollection') {
        feature = /** @type {TopoJSONGeometryCollection} */
            (topoJSONFeatures[i]);
        features.push.apply(features,
            ol.format.TopoJSON.readFeaturesFromGeometryCollection_(
                feature, arcs, scale, translate, opt_options));
      } else {
        feature = /** @type {TopoJSONGeometry} */
            (topoJSONFeatures[i]);
        features.push(ol.format.TopoJSON.readFeatureFromGeometry_(
            feature, arcs, scale, translate, opt_options));
      }
    }
    return features;
  } else {
    return [];
  }
};

/**
 * Apply a linear transform to array of arcs.  The provided array of arcs is
 * modified in place.
 *
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformArcs_ = function(arcs, scale, translate) {
  var i, ii;
  for (i = 0, ii = arcs.length; i < ii; ++i) {
    ol.format.TopoJSON.transformArc_(arcs[i], scale, translate);
  }
};

/**
 * Apply a linear transform to an arc.  The provided arc is modified in place.
 *
 * @param {Array.<ol.Coordinate>} arc Arc.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformArc_ = function(arc, scale, translate) {
  var x = 0;
  var y = 0;
  var vertex;
  var i, ii;
  for (i = 0, ii = arc.length; i < ii; ++i) {
    vertex = arc[i];
    x += vertex[0];
    y += vertex[1];
    vertex[0] = x;
    vertex[1] = y;
    ol.format.TopoJSON.transformVertex_(vertex, scale, translate);
  }
};

/**
 * Apply a linear transform to a vertex.  The provided vertex is modified in
 * place.
 *
 * @param {ol.Coordinate} vertex Vertex.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformVertex_ = function(vertex, scale, translate) {
  vertex[0] = vertex[0] * scale[0] + translate[0];
  vertex[1] = vertex[1] * scale[1] + translate[1];
};

/**
 * Read the projection from a TopoJSON source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} object Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.TopoJSON.prototype.readProjection = function(object) {
  return this.defaultDataProjection;
};

/**
 * @const
 * @private
 * @type {Object.<string, function(TopoJSONGeometry, Array, ...Array): ol.geom.Geometry>}
 */
ol.format.TopoJSON.GEOMETRY_READERS_ = {
  'Point': ol.format.TopoJSON.readPointGeometry_,
  'LineString': ol.format.TopoJSON.readLineStringGeometry_,
  'Polygon': ol.format.TopoJSON.readPolygonGeometry_,
  'MultiPoint': ol.format.TopoJSON.readMultiPointGeometry_,
  'MultiLineString': ol.format.TopoJSON.readMultiLineStringGeometry_,
  'MultiPolygon': ol.format.TopoJSON.readMultiPolygonGeometry_
};
<|endoftext|>


<commit_before>/**
 * @module ol/source/TileArcGISRest
 */

import TileImage from './TileImage.js';
import {appendParams} from '../uri.js';
import {assign} from '../obj.js';
import {createEmpty} from '../extent.js';
import {modulo} from '../math.js';
import {scale as scaleSize, toSize} from '../size.js';
import {hash as tileCoordHash} from '../tilecoord.js';

/**
 * @typedef {Object} Options
 * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
 * @property {number} [cacheSize] Initial tile cache size. Will auto-grow to hold at least the number of tiles in the viewport.
 * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images.  Note that
 * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer.
 * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
 * @property {boolean} [imageSmoothing=true] Enable image smoothing.
 * @property {Object<string,*>} [params] ArcGIS Rest parameters. This field is optional. Service defaults will be
 * used for any fields not specified. `FORMAT` is `PNG32` by default. `F` is `IMAGE` by
 * default. `TRANSPARENT` is `true` by default.  `BBOX`, `SIZE`, `BBOXSR`,
 * and `IMAGESR` will be set dynamically. Set `LAYERS` to
 * override the default service layer visibility. See
 * https://developers.arcgis.com/rest/services-reference/export-map.htm
 * for further reference.
 * @property {boolean} [hidpi=true] Use the `ol/Map#pixelRatio` value when requesting
 * the image from the remote server.
 * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. Base this on the resolutions,
 * tilesize and extent supported by the server.
 * If this is not defined, a default grid will be used: if there is a projection
 * extent, the grid will be based on that; if not, a grid based on a global
 * extent with origin at 0,0 will be used.
 * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
 * The projection code must contain a numeric end portion separated by :
 * or the entire code must form a valid ArcGIS SpatialReference definition.
 * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
 * Higher values can increase reprojection performance, but decrease precision.
 * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL.
 * The default is
 * ```js
 * function(imageTile, src) {
 *   imageTile.getImage().src = src;
 * };
 * ```
 * @property {string} [url] ArcGIS Rest service URL for a Map Service or Image Service. The
 * url should include /MapServer or /ImageServer.
 * @property {boolean} [wrapX=true] Whether to wrap the world horizontally.
 * @property {number} [transition] Duration of the opacity transition for rendering.  To disable the opacity
 * transition, pass `transition: 0`.
 * @property {Array<string>} [urls] ArcGIS Rest service urls. Use this instead of `url` when the ArcGIS
 * Service supports multiple urls for export requests.
 */

/**
 * @classdesc
 * Layer source for tile data from ArcGIS Rest services. Map and Image
 * Services are supported.
 *
 * For cached ArcGIS services, better performance is available using the
 * {@link module:ol/source/XYZ~XYZ} data source.
 * @api
 */
class TileArcGISRest extends TileImage {
  /**
   * @param {Options} [opt_options] Tile ArcGIS Rest options.
   */
  constructor(opt_options) {
    const options = opt_options ? opt_options : {};

    super({
      attributions: options.attributions,
      cacheSize: options.cacheSize,
      crossOrigin: options.crossOrigin,
      imageSmoothing: options.imageSmoothing,
      projection: options.projection,
      reprojectionErrorThreshold: options.reprojectionErrorThreshold,
      tileGrid: options.tileGrid,
      tileLoadFunction: options.tileLoadFunction,
      url: options.url,
      urls: options.urls,
      wrapX: options.wrapX !== undefined ? options.wrapX : true,
      transition: options.transition,
    });

    /**
     * @private
     * @type {!Object}
     */
    this.params_ = options.params || {};

    /**
     * @private
     * @type {boolean}
     */
    this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

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

    this.setKey(this.getKeyForParams_());
  }

  /**
   * @private
   * @return {string} The key for the current params.
   */
  getKeyForParams_() {
    let i = 0;
    const res = [];
    for (const key in this.params_) {
      res[i++] = key + '-' + this.params_[key];
    }
    return res.join('/');
  }

  /**
   * Get the user-provided params, i.e. those passed to the constructor through
   * the "params" option, and possibly updated using the updateParams method.
   * @return {Object} Params.
   * @api
   */
  getParams() {
    return this.params_;
  }

  /**
   * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
   * @param {import("../size.js").Size} tileSize Tile size.
   * @param {import("../extent.js").Extent} tileExtent Tile extent.
   * @param {number} pixelRatio Pixel ratio.
   * @param {import("../proj/Projection.js").default} projection Projection.
   * @param {Object} params Params.
   * @return {string|undefined} Request URL.
   * @private
   */
  getRequestUrl_(
    tileCoord,
    tileSize,
    tileExtent,
    pixelRatio,
    projection,
    params
  ) {
    const urls = this.urls;
    if (!urls) {
      return undefined;
    }

    // ArcGIS Server only wants the numeric portion of the projection ID.
    // (if there is no numeric portion the entire projection code must
    // form a valid ArcGIS SpatialReference definition).
    const srid = projection.getCode().split(/:(?=\d+$)/).pop();

    params['SIZE'] = tileSize[0] + ',' + tileSize[1];
    params['BBOX'] = tileExtent.join(',');
    params['BBOXSR'] = srid;
    params['IMAGESR'] = srid;
    params['DPI'] = Math.round(
      params['DPI'] ? params['DPI'] * pixelRatio : 90 * pixelRatio
    );

    let url;
    if (urls.length == 1) {
      url = urls[0];
    } else {
      const index = modulo(tileCoordHash(tileCoord), urls.length);
      url = urls[index];
    }

    const modifiedUrl = url
      .replace(/MapServer\/?$/, 'MapServer/export')
      .replace(/ImageServer\/?$/, 'ImageServer/exportImage');
    return appendParams(modifiedUrl, params);
  }

  /**
   * Get the tile pixel ratio for this source.
   * @param {number} pixelRatio Pixel ratio.
   * @return {number} Tile pixel ratio.
   */
  getTilePixelRatio(pixelRatio) {
    return this.hidpi_ ? pixelRatio : 1;
  }

  /**
   * Update the user-provided params.
   * @param {Object} params Params.
   * @api
   */
  updateParams(params) {
    assign(this.params_, params);
    this.setKey(this.getKeyForParams_());
  }

  /**
   * @param {import("../tilecoord.js").TileCoord} tileCoord The tile coordinate
   * @param {number} pixelRatio The pixel ratio
   * @param {import("../proj/Projection.js").default} projection The projection
   * @return {string|undefined} The tile URL
   * @override
   */
  tileUrlFunction(tileCoord, pixelRatio, projection) {
    let tileGrid = this.getTileGrid();
    if (!tileGrid) {
      tileGrid = this.getTileGridForProjection(projection);
    }

    if (tileGrid.getResolutions().length <= tileCoord[0]) {
      return undefined;
    }

    if (pixelRatio != 1 && !this.hidpi_) {
      pixelRatio = 1;
    }

    const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
    let tileSize = toSize(tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

    if (pixelRatio != 1) {
      tileSize = scaleSize(tileSize, pixelRatio, this.tmpSize);
    }

    // Apply default params and override with user specified values.
    const baseParams = {
      'F': 'image',
      'FORMAT': 'PNG32',
      'TRANSPARENT': true,
    };
    assign(baseParams, this.params_);

    return this.getRequestUrl_(
      tileCoord,
      tileSize,
      tileExtent,
      pixelRatio,
      projection,
      baseParams
    );
  }
}

export default TileArcGISRest;
<commit_msg>ensure srid is numeric, otherwise use entire code<commit_after>/**
 * @module ol/source/TileArcGISRest
 */

import TileImage from './TileImage.js';
import {appendParams} from '../uri.js';
import {assign} from '../obj.js';
import {createEmpty} from '../extent.js';
import {modulo} from '../math.js';
import {scale as scaleSize, toSize} from '../size.js';
import {hash as tileCoordHash} from '../tilecoord.js';

/**
 * @typedef {Object} Options
 * @property {import("./Source.js").AttributionLike} [attributions] Attributions.
 * @property {number} [cacheSize] Initial tile cache size. Will auto-grow to hold at least the number of tiles in the viewport.
 * @property {null|string} [crossOrigin] The `crossOrigin` attribute for loaded images.  Note that
 * you must provide a `crossOrigin` value if you want to access pixel data with the Canvas renderer.
 * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail.
 * @property {boolean} [imageSmoothing=true] Enable image smoothing.
 * @property {Object<string,*>} [params] ArcGIS Rest parameters. This field is optional. Service defaults will be
 * used for any fields not specified. `FORMAT` is `PNG32` by default. `F` is `IMAGE` by
 * default. `TRANSPARENT` is `true` by default.  `BBOX`, `SIZE`, `BBOXSR`,
 * and `IMAGESR` will be set dynamically. Set `LAYERS` to
 * override the default service layer visibility. See
 * https://developers.arcgis.com/rest/services-reference/export-map.htm
 * for further reference.
 * @property {boolean} [hidpi=true] Use the `ol/Map#pixelRatio` value when requesting
 * the image from the remote server.
 * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. Base this on the resolutions,
 * tilesize and extent supported by the server.
 * If this is not defined, a default grid will be used: if there is a projection
 * extent, the grid will be based on that; if not, a grid based on a global
 * extent with origin at 0,0 will be used.
 * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection.
 * The projection code must contain a numeric end portion separated by :
 * or the entire code must form a valid ArcGIS SpatialReference definition.
 * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels).
 * Higher values can increase reprojection performance, but decrease precision.
 * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL.
 * The default is
 * ```js
 * function(imageTile, src) {
 *   imageTile.getImage().src = src;
 * };
 * ```
 * @property {string} [url] ArcGIS Rest service URL for a Map Service or Image Service. The
 * url should include /MapServer or /ImageServer.
 * @property {boolean} [wrapX=true] Whether to wrap the world horizontally.
 * @property {number} [transition] Duration of the opacity transition for rendering.  To disable the opacity
 * transition, pass `transition: 0`.
 * @property {Array<string>} [urls] ArcGIS Rest service urls. Use this instead of `url` when the ArcGIS
 * Service supports multiple urls for export requests.
 */

/**
 * @classdesc
 * Layer source for tile data from ArcGIS Rest services. Map and Image
 * Services are supported.
 *
 * For cached ArcGIS services, better performance is available using the
 * {@link module:ol/source/XYZ~XYZ} data source.
 * @api
 */
class TileArcGISRest extends TileImage {
  /**
   * @param {Options} [opt_options] Tile ArcGIS Rest options.
   */
  constructor(opt_options) {
    const options = opt_options ? opt_options : {};

    super({
      attributions: options.attributions,
      cacheSize: options.cacheSize,
      crossOrigin: options.crossOrigin,
      imageSmoothing: options.imageSmoothing,
      projection: options.projection,
      reprojectionErrorThreshold: options.reprojectionErrorThreshold,
      tileGrid: options.tileGrid,
      tileLoadFunction: options.tileLoadFunction,
      url: options.url,
      urls: options.urls,
      wrapX: options.wrapX !== undefined ? options.wrapX : true,
      transition: options.transition,
    });

    /**
     * @private
     * @type {!Object}
     */
    this.params_ = options.params || {};

    /**
     * @private
     * @type {boolean}
     */
    this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

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

    this.setKey(this.getKeyForParams_());
  }

  /**
   * @private
   * @return {string} The key for the current params.
   */
  getKeyForParams_() {
    let i = 0;
    const res = [];
    for (const key in this.params_) {
      res[i++] = key + '-' + this.params_[key];
    }
    return res.join('/');
  }

  /**
   * Get the user-provided params, i.e. those passed to the constructor through
   * the "params" option, and possibly updated using the updateParams method.
   * @return {Object} Params.
   * @api
   */
  getParams() {
    return this.params_;
  }

  /**
   * @param {import("../tilecoord.js").TileCoord} tileCoord Tile coordinate.
   * @param {import("../size.js").Size} tileSize Tile size.
   * @param {import("../extent.js").Extent} tileExtent Tile extent.
   * @param {number} pixelRatio Pixel ratio.
   * @param {import("../proj/Projection.js").default} projection Projection.
   * @param {Object} params Params.
   * @return {string|undefined} Request URL.
   * @private
   */
  getRequestUrl_(
    tileCoord,
    tileSize,
    tileExtent,
    pixelRatio,
    projection,
    params
  ) {
    const urls = this.urls;
    if (!urls) {
      return undefined;
    }

    // ArcGIS Server only wants the numeric portion of the projection ID.
    // (if there is no numeric portion the entire projection code must
    // form a valid ArcGIS SpatialReference definition).
    const srid = projection
      .getCode()
      .split(/:(?=\d+$)/)
      .pop();

    params['SIZE'] = tileSize[0] + ',' + tileSize[1];
    params['BBOX'] = tileExtent.join(',');
    params['BBOXSR'] = srid;
    params['IMAGESR'] = srid;
    params['DPI'] = Math.round(
      params['DPI'] ? params['DPI'] * pixelRatio : 90 * pixelRatio
    );

    let url;
    if (urls.length == 1) {
      url = urls[0];
    } else {
      const index = modulo(tileCoordHash(tileCoord), urls.length);
      url = urls[index];
    }

    const modifiedUrl = url
      .replace(/MapServer\/?$/, 'MapServer/export')
      .replace(/ImageServer\/?$/, 'ImageServer/exportImage');
    return appendParams(modifiedUrl, params);
  }

  /**
   * Get the tile pixel ratio for this source.
   * @param {number} pixelRatio Pixel ratio.
   * @return {number} Tile pixel ratio.
   */
  getTilePixelRatio(pixelRatio) {
    return this.hidpi_ ? pixelRatio : 1;
  }

  /**
   * Update the user-provided params.
   * @param {Object} params Params.
   * @api
   */
  updateParams(params) {
    assign(this.params_, params);
    this.setKey(this.getKeyForParams_());
  }

  /**
   * @param {import("../tilecoord.js").TileCoord} tileCoord The tile coordinate
   * @param {number} pixelRatio The pixel ratio
   * @param {import("../proj/Projection.js").default} projection The projection
   * @return {string|undefined} The tile URL
   * @override
   */
  tileUrlFunction(tileCoord, pixelRatio, projection) {
    let tileGrid = this.getTileGrid();
    if (!tileGrid) {
      tileGrid = this.getTileGridForProjection(projection);
    }

    if (tileGrid.getResolutions().length <= tileCoord[0]) {
      return undefined;
    }

    if (pixelRatio != 1 && !this.hidpi_) {
      pixelRatio = 1;
    }

    const tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
    let tileSize = toSize(tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

    if (pixelRatio != 1) {
      tileSize = scaleSize(tileSize, pixelRatio, this.tmpSize);
    }

    // Apply default params and override with user specified values.
    const baseParams = {
      'F': 'image',
      'FORMAT': 'PNG32',
      'TRANSPARENT': true,
    };
    assign(baseParams, this.params_);

    return this.getRequestUrl_(
      tileCoord,
      tileSize,
      tileExtent,
      pixelRatio,
      projection,
      baseParams
    );
  }
}

export default TileArcGISRest;
<|endoftext|>


<commit_before>var irc = require('irc');
var Slack = require('slack-client');
var assert = require('assert');

function Bot(config) {

  this.config = config;

  assert(config.slack.token !== "", "Slack token not defined");
  assert(config.irc.nick !== "", "IRC nick not defined");

  this.irc = new irc.Client(config.irc.server, config.irc.nick, {
    port: config.irc.port || 6667,
    channels: (config.irc.channel.protected ? undefined : [config.irc.channel.name]),
    localAddress: config.irc.localAddress,
    userName: config.irc.userName,
    realName: config.irc.realName,
    debug: config.irc.debug,
    encoding: config.irc.encoding,
    autoConnect: false
  });

  this.slack = new Slack(config.slack.token, config.slack.autoReconnect,
    config.slack.autoMark);

  this.addListeners();
  this.connect();
}

Bot.prototype.addListeners = function() {
  this.slack.on('open', function() {
    console.log("Connected to Slack");
  });

  this.slack.on('error', function(err) {
    console.error("Slack error", err);
  });

  this.irc.on('error', function(err) {
    console.error("IRC error", err);
  });

  // Handler for Slack messages.
  this.slack.on('message', function(message) {
    this.sendToIRC(message);
  }.bind(this));

  // Handler for IRC channel messages.
  this.irc.on('message', function(from, to, text, message) {
    this.sendToSlack(from, text);
  }.bind(this));
}

Bot.prototype.connect = function() {
  console.log("Connecting servers...");
  this.slack.login();

  // Join the password-protected channel after the IRC server has been connected.
  if (this.config.irc.channel.protected == true) {
    this.irc.connect(function() {
      this.send('JOIN', this.config.irc.channel.name, this.config.irc.channel.password);
    });
  } else {
    this.irc.connect();
  }
}

Bot.prototype.sendToSlack = function(from, text) {
  var chan = this.slack.getChannelByName(this.config.slack.channel);
  chan.send(from + '@IRC> ' + text);
}

Bot.prototype.sendToIRC = function(message) {
  // Skip unsupported subtypes
  if (message.subtype != null && message.subtype !== 'bot_message') {
    return;
  }

  // Skip hidden messages, such as edited messages.
  if (message.hidden != null && message.hidden == true) {
    return;
  }

  var channel = this.slack.getChannelGroupOrDMByID(message.channel);
  if (!channel || (channel.name !== this.config.slack.channel)) {
    return;
  }

  // Bot messages
  if (message.subtype != null && message.subtype === 'bot_message') {
    if (message.attachments[0] != null) {
      this.irc.say(this.config.irc.channel.name, "bot> " + message.attachments[0].fallback);
    }
    return;
  }

  // User messages
  var body = this.decode(message.getBody());

  var user = this.slack.getUserByID(message.user);
  var msg = user.name + '> ' + body;
  this.irc.say(this.config.irc.channel.name, msg);
}

Bot.prototype.decode = function(str) {
  return str
    .replace(/>/g, '>')
    .replace(/</g, '<')
    .replace(/&/g, '&')
    .replace(/<@(U\w+)\|?(\w+)?>/g, function(match, uid, plain) {
      return plain || this.slack.getUserByID(uid).name;
    }.bind(this))
    .replace(/<(\S+)>/g, function(m, url) {
      return url;
    })
    .replace(/:smile:|:smiley:|:blush:|:relaxed:|:relieved:/g, ':)')
    .replace(/:confused:|:disappointed:|:worried:/g, ':(' )
    .replace(/:wink:/g, ';)')
    .replace(/:joy:/g, ':´D')
    .replace(/:grinning:|:grin:|:laughing:/g, ':D')
    .replace(/:stuck_out_tongue:/g, ':P')
    .replace(/:stuck_out_tongue_winking_eye:/g, ';P')
    .replace(/:stuck_out_tongue_closed_eyes:/g, 'XP')
    .replace(/:open_mouth:|:hushed:|:astonished:|:scream:|:dizzy_face:|:flushed:/g, ':O')
    .replace(/:rage:|:imp:/g, '>:(')
    .replace(/:smiling_imp:/g, '>:)');
};

module.exports = Bot;
<commit_msg>Proper checks for bot messages. Fixes #2<commit_after>var irc = require('irc');
var Slack = require('slack-client');
var assert = require('assert');

function Bot(config) {

  this.config = config;

  assert(config.slack.token !== "", "Slack token not defined");
  assert(config.irc.nick !== "", "IRC nick not defined");

  this.irc = new irc.Client(config.irc.server, config.irc.nick, {
    port: config.irc.port || 6667,
    channels: (config.irc.channel.protected ? undefined : [config.irc.channel.name]),
    localAddress: config.irc.localAddress,
    userName: config.irc.userName,
    realName: config.irc.realName,
    debug: config.irc.debug,
    encoding: config.irc.encoding,
    autoConnect: false
  });

  this.slack = new Slack(config.slack.token, config.slack.autoReconnect,
    config.slack.autoMark);

  this.addListeners();
  this.connect();
}

Bot.prototype.addListeners = function() {
  this.slack.on('open', function() {
    console.log("Connected to Slack");
  });

  this.slack.on('error', function(err) {
    console.error("Slack error", err);
  });

  this.irc.on('error', function(err) {
    console.error("IRC error", err);
  });

  // Handler for Slack messages.
  this.slack.on('message', function(message) {
    this.sendToIRC(message);
  }.bind(this));

  // Handler for IRC channel messages.
  this.irc.on('message', function(from, to, text, message) {
    this.sendToSlack(from, text);
  }.bind(this));
};

Bot.prototype.connect = function() {
  console.log("Connecting servers...");
  this.slack.login();

  // Join the password-protected channel after the IRC server has been connected.
  if (this.config.irc.channel.protected === true) {
    this.irc.connect(function() {
      this.send('JOIN', this.config.irc.channel.name, this.config.irc.channel.password);
    });
  } else {
    this.irc.connect();
  }
};

Bot.prototype.sendToSlack = function(from, text) {
  var chan = this.slack.getChannelByName(this.config.slack.channel);
  chan.send(from + '@IRC> ' + text);
};

Bot.prototype.sendToIRC = function(message) {
  // Skip unsupported subtypes
  if (message.subtype !== null && message.subtype !== 'bot_message') {
    return;
  }

  // Skip hidden messages, such as edited messages.
  if (message.hidden !== null && message.hidden === true) {
    return;
  }

  var channel = this.slack.getChannelGroupOrDMByID(message.channel);
  if (!channel || (channel.name !== this.config.slack.channel)) {
    return;
  }

  // Bot messages
  if (message.subtype !== null && message.subtype === 'bot_message') {
    if (message.attachments !== null && message.attachments.length > 0 && message.attachments[0].fallback) {
      this.irc.say(this.config.irc.channel.name, "bot> " + message.attachments[0].fallback);
    }
    return;
  }

  // User messages
  var body = this.decode(message.getBody());

  var user = this.slack.getUserByID(message.user);
  var msg = user.name + '> ' + body;
  this.irc.say(this.config.irc.channel.name, msg);
};

Bot.prototype.decode = function(str) {
  return str
    .replace(/>/g, '>')
    .replace(/</g, '<')
    .replace(/&/g, '&')
    .replace(/<@(U\w+)\|?(\w+)?>/g, function(match, uid, plain) {
      return plain || this.slack.getUserByID(uid).name;
    }.bind(this))
    .replace(/<(\S+)>/g, function(m, url) {
      return url;
    })
    .replace(/:smile:|:smiley:|:blush:|:relaxed:|:relieved:/g, ':)')
    .replace(/:confused:|:disappointed:|:worried:/g, ':(' )
    .replace(/:wink:/g, ';)')
    .replace(/:joy:/g, ':´D')
    .replace(/:grinning:|:grin:|:laughing:/g, ':D')
    .replace(/:stuck_out_tongue:/g, ':P')
    .replace(/:stuck_out_tongue_winking_eye:/g, ';P')
    .replace(/:stuck_out_tongue_closed_eyes:/g, 'XP')
    .replace(/:open_mouth:|:hushed:|:astonished:|:scream:|:dizzy_face:|:flushed:/g, ':O')
    .replace(/:rage:|:imp:/g, '>:(')
    .replace(/:smiling_imp:/g, '>:)');
};

module.exports = Bot;
<|endoftext|>


<commit_before>var events = require('events'),
  util = require('util'),
  url_parser = require('./url_parser'),
  async = require('async'),
  moment = require('moment'),
  condition_evaluator = require('./condition_evaluator'),
  logger = require('log4node');

function BaseComponent() {
  events.EventEmitter.call(this);
  this.setMaxListeners(0);
  this.required_libs = {};
  this.config = {
    optional_params: [],
    required_params: [],
    default_values: {},
    arrays: [],
    hashes: [],
  };
  this.start_hooks = [];
  this.config_hooks = [];
}

util.inherits(BaseComponent, events.EventEmitter);

BaseComponent.prototype.requireLib = function(name) {
  if (!this.required_libs[name]) {
    try {
      this.required_libs[name] = require(name);
    }
    catch (e) {
      console.error('Unable to load module', name);
      throw e;
    }
  }
  return this.required_libs[name];
};

BaseComponent.prototype.mergeConfig = function(config) {
  ['name', 'host_field', 'port_field', 'allow_empty_host'].forEach(function(x) {
    if (config[x]) {
      this.config[x] = config[x];
    }
  }.bind(this));
  ['required_params', 'optional_params', 'arrays', 'hashes'].forEach(function(x) {
    if (config[x]) {
      this.config[x] = this.config[x].concat(config[x]);
    }
  }.bind(this));
  if (config.default_values) {
    for (var x in config.default_values) {
      this.config.default_values[x] = config.default_values[x];
    }
  }
  if (config.start_hook) {
    this.start_hooks.push(config.start_hook);
  }
  if (config.config_hook) {
    this.config_hooks.push(config.config_hook);
  }
};

BaseComponent.prototype.processValue = function(x) {
  if (x === 'true') {
    return true;
  }
  else if (x === 'false') {
    return false;
  }
  return x;
};

BaseComponent.prototype.loadConfig = function(url, callback) {
  this.message_filtering = {};

  var val, i, res;

  if (url.length === 0) {
    if (this.config.host_field || this.config.port_field || this.config.required_params.length > 0) {
      return callback(new Error('No empty url for ' + this.config.name));
    }
    this.parsed_url = {
      params: [],
    };
  }
  else {
    this.parsed_url = url_parser.processUrlContent(url);
    if (!this.parsed_url) {
      return callback(new Error('Unable to parse config : ' + url));
    }
  }

  if (this.config.host_field) {
    if (!this.parsed_url.host && this.parsed_url.params[this.config.host_field]) {
      this.parsed_url.host = this.parsed_url.params[this.config.host_field];
    }
    if (!this.parsed_url.host && !this.config.allow_empty_host) {
      return callback(new Error('No host found in url ' + url));
    }

    if (this.config.port_field && this.config.port_field !== -1) {
      var p = url_parser.extractPortNumber(this.parsed_url.host);
      if (!p && this.parsed_url.params[this.config.port_field]) {
        p = {host: this.parsed_url.host, port: this.parsed_url.params[this.config.port_field]};
      }
      if (!p) {
        return callback(new Error('Unable to extract port from ' + this.parsed_url.host));
      }
      this[this.config.host_field] = p.host;
      this[this.config.port_field] = p.port;
    }
    else {
      this[this.config.host_field] = this.parsed_url.host;
    }
  }

  if (this.config.required_params) {
    for (i = 0; i < this.config.required_params.length; i++) {
      val = this.parsed_url.params[this.config.required_params[i]];
      if (val) {
        this[this.config.required_params[i]] = this.processValue(val);
      }
    }
  }

  if (this.config.optional_params) {
    for (i = 0; i < this.config.optional_params.length; i++) {
      val = this.parsed_url.params[this.config.optional_params[i]];
      if (val !== undefined) {
        this[this.config.optional_params[i]] = this.processValue(val);
      }
    }
  }

  if (this.config.default_values) {
    for (i in this.config.default_values) {
      if (this[i] === undefined) {
        this[i] = this.config.default_values[i];
      }
    }
  }

  for (i in this.config.arrays) {
    if (this[this.config.arrays[i]]) {
      if (!Array.isArray(this[this.config.arrays[i]])) {
        this[this.config.arrays[i]] = this[this.config.arrays[i]].split(',');
      }
    }
  }

  for (i in this.config.hashes) {
    if (this[this.config.hashes[i]]) {
      var x = {};
      if (typeof(this[this.config.hashes[i]]) !== 'object') {
        var v = this[this.config.hashes[i]].split(',');
        for(var j in v) {
          var splitted = v[j].split(':');
          x[splitted[0]] = splitted[1];
        }
      }
      this[this.config.hashes[i]] = x;
    }
  }

  if (this.parsed_url.params.only_type) {
    this.message_filtering.only_type = this.parsed_url.params.only_type;
  }

  for (i in this.parsed_url.params) {
    res = i.match(/^only_field_exist_(.+)$/);
    if (res) {
      if (!this.message_filtering.only_field_exist) {
        this.message_filtering.only_field_exist = [];
      }
      this.message_filtering.only_field_exist.push(res[1]);
    }
    res = i.match(/^only_field_equal_(.+)$/);
    if (res) {
      if (!this.message_filtering.only_field_equal) {
        this.message_filtering.only_field_equal = {};
      }
      this.message_filtering.only_field_equal[res[1]] = this.parsed_url.params[i];
    }
    res = i.match(/^only_field_match_(.+)$/);
    if (res) {
      if (!this.message_filtering.only_field_match) {
        this.message_filtering.only_field_match = {};
      }
      this.message_filtering.only_field_match[res[1]] = new RegExp(this.parsed_url.params[i]);
    }

    if (this.parsed_url.params.__dynamic_eval__) {
      this.__dynamic_eval__ = JSON.parse(this.parsed_url.params.__dynamic_eval__);
    }
  }

  async.eachSeries(this.config_hooks, function(hook, callback) {
    hook.call(this, callback);
  }.bind(this), function(err) {
    if (err) {
      return callback(err);
    }

    if (this.config.required_params) {
      for (var i = 0; i < this.config.required_params.length; i++) {
        if (!this[this.config.required_params[i]]) {
          return callback(new Error('You have to specify ' + this.config.required_params[i] + ' in url ' + url));
        }
      }
    }

    async.eachSeries(this.start_hooks, function(hook, callback) {
      hook.call(this, callback);
    }.bind(this), callback);
  }.bind(this));
};

BaseComponent.prototype.processMessage = function(data) {
  var i;

  if (this.message_filtering.only_type && this.message_filtering.only_type !== data.type) {
    return false;
  }

  if (this.message_filtering.only_field_exist) {
    for (i = 0; i < this.message_filtering.only_field_exist.length; i++) {
      if (!data[this.message_filtering.only_field_exist[i]]) {
        return false;
      }
    }
  }

  if (this.message_filtering.only_field_equal) {
    for (i in this.message_filtering.only_field_equal) {
      if (!data[i] || data[i] !== this.message_filtering.only_field_equal[i]) {
        return false;
      }
    }
  }

  if (this.message_filtering.only_field_match) {
    for (i in this.message_filtering.only_field_match) {
      if (!data[i] || ! this.message_filtering.only_field_match[i].exec(data[i])) {
        return false;
      }
    }
  }

  if (this.__dynamic_eval__) {
    try {
      var keep = true;
      for(i = 0; i < this.__dynamic_eval__.false_clauses.length; i ++) {
        var x = this.__dynamic_eval__.false_clauses[i];
        if (condition_evaluator.compute(x, data) === true) {
          keep = false;
          break;
        }
      }
      if (keep && this.__dynamic_eval__.true_clause) {
        if (condition_evaluator.compute(this.__dynamic_eval__.true_clause, data) === false) {
          keep = false;
        }
      }
      if (!keep) {
        return false;
      }
    }
    catch(e) {
      logger.error('Conditional error', e);
      return false;
    }
  }

  return true;
};

var replace_cache = {};

BaseComponent.prototype.replaceByFields = function(data, s) {
  if (!replace_cache[s]) {
    var x = [];
    var keep = s;
    while(true) {
      var result = keep.indexOf('#{');
      var end = result === -1 ? -1 : keep.substring(result + 2).indexOf('}');
      if (result !== -1 && end !== -1) {
        x.push({
          type: 'string',
          string: keep.substring(0, result),
        });
        var key = keep.substring(result + 2, result + end + 2);
        var date = key.match(/now:(.*)$/);
        if (date) {
          x.push({
            type: 'now',
            format: date[1],
          });
        }
        else {
          x.push({
            type: 'value',
            value: key,
          });
        }
        keep = keep.substring(result + end + 3);
      }
      else {
        if (keep.length > 0) {
          x.push({
            type: 'string',
            string: keep,
          });
        }
        break;
      }
    }
    replace_cache[s] = x;
  }
  var cache = replace_cache[s];
  var r = '';
  for(var i = 0; i < cache.length; i ++) {
    if (cache[i].type === 'string') {
      r += cache[i].string;
    }
    else if (cache[i].type === 'now') {
      r += moment().format(cache[i].format);
    }
    else if (cache[i].type === 'value') {
      if (data[cache[i].value]) {
        r += data[cache[i].value];
      }
      else {
        logger.debug('Unable to find field', cache[i].value);
        return undefined;
      }
    }
  }
  return r;
};

BaseComponent.prototype.add_tags_function = function(data, field) {
  if (this[field]) {
    if (! data.tags) {
      data.tags = [];
    }
    this[field].forEach(function(tag) {
      data.tags.push(tag);
    });
  }
};

BaseComponent.prototype.remove_tags_function = function(data) {
  if (this.remove_tags) {
    if (! data.tags) {
      data.tags = [];
    }
    this.remove_tags.forEach(function(tag) {
      var index = data.tags.indexOf(tag);
      if (index > -1) {
        data.tags.splice(index, 1);
      }
    });
  }
};

BaseComponent.prototype.add_fields_function = function(data) {
  if (this.add_fields) {
    Object.keys(this.add_fields).forEach(function(f) {
      data[f] = this.replaceByFields(data, this.add_fields[f]);
    }.bind(this));
  }
};

BaseComponent.prototype.remove_fields_function = function(data) {
  if (this.remove_fields) {
    this.remove_fields.forEach(function(f) {
      delete data[f];
    }.bind(this));
  }
};

exports.BaseComponent = BaseComponent;
<commit_msg>support for dot notation in field extraction<commit_after>var events = require('events'),
  util = require('util'),
  url_parser = require('./url_parser'),
  async = require('async'),
  moment = require('moment'),
  condition_evaluator = require('./condition_evaluator'),
  logger = require('log4node');

function BaseComponent() {
  events.EventEmitter.call(this);
  this.setMaxListeners(0);
  this.required_libs = {};
  this.config = {
    optional_params: [],
    required_params: [],
    default_values: {},
    arrays: [],
    hashes: [],
  };
  this.start_hooks = [];
  this.config_hooks = [];
}

util.inherits(BaseComponent, events.EventEmitter);

BaseComponent.prototype.requireLib = function(name) {
  if (!this.required_libs[name]) {
    try {
      this.required_libs[name] = require(name);
    }
    catch (e) {
      console.error('Unable to load module', name);
      throw e;
    }
  }
  return this.required_libs[name];
};

BaseComponent.prototype.mergeConfig = function(config) {
  ['name', 'host_field', 'port_field', 'allow_empty_host'].forEach(function(x) {
    if (config[x]) {
      this.config[x] = config[x];
    }
  }.bind(this));
  ['required_params', 'optional_params', 'arrays', 'hashes'].forEach(function(x) {
    if (config[x]) {
      this.config[x] = this.config[x].concat(config[x]);
    }
  }.bind(this));
  if (config.default_values) {
    for (var x in config.default_values) {
      this.config.default_values[x] = config.default_values[x];
    }
  }
  if (config.start_hook) {
    this.start_hooks.push(config.start_hook);
  }
  if (config.config_hook) {
    this.config_hooks.push(config.config_hook);
  }
};

BaseComponent.prototype.processValue = function(x) {
  if (x === 'true') {
    return true;
  }
  else if (x === 'false') {
    return false;
  }
  return x;
};

BaseComponent.prototype.loadConfig = function(url, callback) {
  this.message_filtering = {};

  var val, i, res;

  if (url.length === 0) {
    if (this.config.host_field || this.config.port_field || this.config.required_params.length > 0) {
      return callback(new Error('No empty url for ' + this.config.name));
    }
    this.parsed_url = {
      params: [],
    };
  }
  else {
    this.parsed_url = url_parser.processUrlContent(url);
    if (!this.parsed_url) {
      return callback(new Error('Unable to parse config : ' + url));
    }
  }

  if (this.config.host_field) {
    if (!this.parsed_url.host && this.parsed_url.params[this.config.host_field]) {
      this.parsed_url.host = this.parsed_url.params[this.config.host_field];
    }
    if (!this.parsed_url.host && !this.config.allow_empty_host) {
      return callback(new Error('No host found in url ' + url));
    }

    if (this.config.port_field && this.config.port_field !== -1) {
      var p = url_parser.extractPortNumber(this.parsed_url.host);
      if (!p && this.parsed_url.params[this.config.port_field]) {
        p = {host: this.parsed_url.host, port: this.parsed_url.params[this.config.port_field]};
      }
      if (!p) {
        return callback(new Error('Unable to extract port from ' + this.parsed_url.host));
      }
      this[this.config.host_field] = p.host;
      this[this.config.port_field] = p.port;
    }
    else {
      this[this.config.host_field] = this.parsed_url.host;
    }
  }

  if (this.config.required_params) {
    for (i = 0; i < this.config.required_params.length; i++) {
      val = this.parsed_url.params[this.config.required_params[i]];
      if (val) {
        this[this.config.required_params[i]] = this.processValue(val);
      }
    }
  }

  if (this.config.optional_params) {
    for (i = 0; i < this.config.optional_params.length; i++) {
      val = this.parsed_url.params[this.config.optional_params[i]];
      if (val !== undefined) {
        this[this.config.optional_params[i]] = this.processValue(val);
      }
    }
  }

  if (this.config.default_values) {
    for (i in this.config.default_values) {
      if (this[i] === undefined) {
        this[i] = this.config.default_values[i];
      }
    }
  }

  for (i in this.config.arrays) {
    if (this[this.config.arrays[i]]) {
      if (!Array.isArray(this[this.config.arrays[i]])) {
        this[this.config.arrays[i]] = this[this.config.arrays[i]].split(',');
      }
    }
  }

  for (i in this.config.hashes) {
    if (this[this.config.hashes[i]]) {
      var x = {};
      if (typeof(this[this.config.hashes[i]]) !== 'object') {
        var v = this[this.config.hashes[i]].split(',');
        for(var j in v) {
          var splitted = v[j].split(':');
          x[splitted[0]] = splitted[1];
        }
      }
      this[this.config.hashes[i]] = x;
    }
  }

  if (this.parsed_url.params.only_type) {
    this.message_filtering.only_type = this.parsed_url.params.only_type;
  }

  for (i in this.parsed_url.params) {
    res = i.match(/^only_field_exist_(.+)$/);
    if (res) {
      if (!this.message_filtering.only_field_exist) {
        this.message_filtering.only_field_exist = [];
      }
      this.message_filtering.only_field_exist.push(res[1]);
    }
    res = i.match(/^only_field_equal_(.+)$/);
    if (res) {
      if (!this.message_filtering.only_field_equal) {
        this.message_filtering.only_field_equal = {};
      }
      this.message_filtering.only_field_equal[res[1]] = this.parsed_url.params[i];
    }
    res = i.match(/^only_field_match_(.+)$/);
    if (res) {
      if (!this.message_filtering.only_field_match) {
        this.message_filtering.only_field_match = {};
      }
      this.message_filtering.only_field_match[res[1]] = new RegExp(this.parsed_url.params[i]);
    }

    if (this.parsed_url.params.__dynamic_eval__) {
      this.__dynamic_eval__ = JSON.parse(this.parsed_url.params.__dynamic_eval__);
    }
  }

  async.eachSeries(this.config_hooks, function(hook, callback) {
    hook.call(this, callback);
  }.bind(this), function(err) {
    if (err) {
      return callback(err);
    }

    if (this.config.required_params) {
      for (var i = 0; i < this.config.required_params.length; i++) {
        if (!this[this.config.required_params[i]]) {
          return callback(new Error('You have to specify ' + this.config.required_params[i] + ' in url ' + url));
        }
      }
    }

    async.eachSeries(this.start_hooks, function(hook, callback) {
      hook.call(this, callback);
    }.bind(this), callback);
  }.bind(this));
};

BaseComponent.prototype.processMessage = function(data) {
  var i;

  if (this.message_filtering.only_type && this.message_filtering.only_type !== data.type) {
    return false;
  }

  if (this.message_filtering.only_field_exist) {
    for (i = 0; i < this.message_filtering.only_field_exist.length; i++) {
      if (!data[this.message_filtering.only_field_exist[i]]) {
        return false;
      }
    }
  }

  if (this.message_filtering.only_field_equal) {
    for (i in this.message_filtering.only_field_equal) {
      if (!data[i] || data[i] !== this.message_filtering.only_field_equal[i]) {
        return false;
      }
    }
  }

  if (this.message_filtering.only_field_match) {
    for (i in this.message_filtering.only_field_match) {
      if (!data[i] || ! this.message_filtering.only_field_match[i].exec(data[i])) {
        return false;
      }
    }
  }

  if (this.__dynamic_eval__) {
    try {
      var keep = true;
      for(i = 0; i < this.__dynamic_eval__.false_clauses.length; i ++) {
        var x = this.__dynamic_eval__.false_clauses[i];
        if (condition_evaluator.compute(x, data) === true) {
          keep = false;
          break;
        }
      }
      if (keep && this.__dynamic_eval__.true_clause) {
        if (condition_evaluator.compute(this.__dynamic_eval__.true_clause, data) === false) {
          keep = false;
        }
      }
      if (!keep) {
        return false;
      }
    }
    catch(e) {
      logger.error('Conditional error', e);
      return false;
    }
  }

  return true;
};

var deepFindField = function( propertyName, object ) {
  var parts = propertyName.split( "." ),
    length = parts.length,
    i,
    property = object || this;
  for ( i = 0; i < length; i++ ) {
    property = property[parts[i]];
  }
  return property;
};

var replace_cache = {};

BaseComponent.prototype.replaceByFields = function(data, s) {
  if (!replace_cache[s]) {
    var x = [];
    var keep = s;
    while(true) {
      var result = keep.indexOf('#{');
      var end = result === -1 ? -1 : keep.substring(result + 2).indexOf('}');
      if (result !== -1 && end !== -1) {
        x.push({
          type: 'string',
          string: keep.substring(0, result),
        });
        var key = keep.substring(result + 2, result + end + 2);
        var date = key.match(/now:(.*)$/);
        if (date) {
          x.push({
            type: 'now',
            format: date[1],
          });
        }
        else {
          x.push({
            type: 'value',
            value: key,
          });
        }
        keep = keep.substring(result + end + 3);
      }
      else {
        if (keep.length > 0) {
          x.push({
            type: 'string',
            string: keep,
          });
        }
        break;
      }
    }
    replace_cache[s] = x;
  }
  var cache = replace_cache[s];
  var r = '';
  for(var i = 0; i < cache.length; i ++) {
    if (cache[i].type === 'string') {
      r += cache[i].string;
    }
    else if (cache[i].type === 'now') {
      r += moment().format(cache[i].format);
    }
    else if (cache[i].type === 'value') {
      if (cache[i].value.indexOf('.')) {
        var dp = deepFindField(cache[i].value, data);
        if (dp) r += dp;
      }
      else if (data[cache[i].value]) {
        r += data[cache[i].value];
      }
      else {
        logger.debug('Unable to find field', cache[i].value);
        return undefined;
      }
    }
  }
  return r;
};

BaseComponent.prototype.add_tags_function = function(data, field) {
  if (this[field]) {
    if (! data.tags) {
      data.tags = [];
    }
    this[field].forEach(function(tag) {
      data.tags.push(tag);
    });
  }
};

BaseComponent.prototype.remove_tags_function = function(data) {
  if (this.remove_tags) {
    if (! data.tags) {
      data.tags = [];
    }
    this.remove_tags.forEach(function(tag) {
      var index = data.tags.indexOf(tag);
      if (index > -1) {
        data.tags.splice(index, 1);
      }
    });
  }
};

BaseComponent.prototype.add_fields_function = function(data) {
  if (this.add_fields) {
    Object.keys(this.add_fields).forEach(function(f) {
      data[f] = this.replaceByFields(data, this.add_fields[f]);
    }.bind(this));
  }
};

BaseComponent.prototype.remove_fields_function = function(data) {
  if (this.remove_fields) {
    this.remove_fields.forEach(function(f) {
      delete data[f];
    }.bind(this));
  }
};

exports.BaseComponent = BaseComponent;
<|endoftext|>


<commit_before>var stopwords = require('natural').stopwords;
var mapreduce = require('./mapreduce.js');
var logger = require('../logger.js');
var scontext = require('search-context');
var totalDocs = 0;

//used on startup
exports.setTotalDocs = function (td) {
  totalDocs = td;
}

exports.search = function (indexes, indexesMultiply, q, callback) {
  var cleanQuery = {};
  var canSearch = true;
  var keySet = [];

  var getDocumentFreqencies = function(indexesMultiply, callback) {
    //Check document frequencies
    var err = false;
    var docFreqs = {};
    var tfsets = [];
    var totalHits = 0;
    console.log('HEEEEE')
    indexesMultiply.get(keySet, function(err, data) {
      for (k in data) {
        if (data[k] == null) {
          //return a zero result
          docFreqs[keySet[i]] = 0;
          logger.debug('KEY ' + k + ' NOT FOUND IN DICTIONARY');
        }
        else {
      //    totalHits = data[k].length;
          docFreqs[k] = data[k].length;
          tfsets.push(data[k]);
        }
      }
      var intersection = tfsets[0];
      for (var i = 1; i < tfsets.length; i++) {
        intersection = intersectionDestructive(intersection, tfsets[i]);
      }

      if(intersection === undefined)
        err = true;
      else
        totalHits = intersection.length;

      //    console.log('tntersection: ' + intersection);
      //    pickSearch(docFreqs);
      callback(err, {docFreqs:docFreqs, allDocsIDsInResultSet:intersection});
    });
  };

  var getFacets = function(q, indexes, callback) {
    //if no facet request callback and return
    if (!q.facets){callback({});return;}

    var facetLength = 10;
    if (q.facetLength) facetLength = q.facetLength;

    var sortOnValueDesc = function(a,b) {
      if (a.value  b.value)
        return -1;
      return 0;
    }
    var sortOnValueAsc = function(a,b) {
      if (a.value  b.value)
        return 1;
      return 0;
    }
    var sortOnKeyDesc = function(a,b) {
      if (a.key  b.key)
        return -1;
      return 0;
    }
    var sortOnKeyAsc = function(a,b) {
      if (a.key  b.key)
        return 1;
      return 0;
    }

    var facetKeySet = [];
    for (var searchField in q.query) {
      for (var h = 0; h < q.query[searchField].length; h++) {
        var queryToken = q.query[searchField][h];
        for (var i = 0; i < q.facets.length; i++) {
          facetKeySet.push('TF~' + searchField + '~' + queryToken + '~' + q.facets[i]);
        }
      }
    }

    var facetsSoFar = {};
    for (var i = 0; i < q.facets.length; i++)
      facetsSoFar[q.facets[i]] = {};

    //could be speeded up by always taking the least frequent keys first?
    var getFacetSets = function(fks, counter) {

      var sortOnValueDesc = function(a,b) {
        if (a.value  b.value)
          return -1;
        return 0;
      }
      var sortOnValueAsc = function(a,b) {
        if (a.value  b.value)
          return 1;
        return 0;
      }
      var sortOnKeyDesc = function(a,b) {
        if (a.key  b.key)
          return -1;
        return 0;
      }
      var sortOnKeyAsc = function(a,b) {
        if (a.key  b.key)
          return 1;
        return 0;
      }

      var thisFacetCat = fks[counter].split('~')[3];
      indexes.createReadStream({
        valueEncoding: 'json',
        limit: -1,
        start: fks[counter] + '~',
        end: fks[counter] + '~~'})
        .on('data', function (data) {
          var thisFacetName = data.key.split('~')[4];
          if (!facetsSoFar[thisFacetCat][thisFacetName]) {
            facetsSoFar[thisFacetCat][thisFacetName] = data.value;
          }
          else {
            var intersection = intersectionDestructive(facetsSoFar[thisFacetCat][thisFacetName], data.value);
            if (intersection.length > 0)
              facetsSoFar[thisFacetCat][thisFacetName] = intersection;
            else
              delete facetsSoFar[thisFacetCat][thisFacetName];
          }
        })
        .on('end', function () {
          if (++counter < fks.length) {getFacetSets(facetKeySet, counter);}
          else {
            //flatten sets to counts
            var finalFacets = {};
            for (k in facetsSoFar) {
              finalFacets[k] = [];
              for (kk in facetsSoFar[k]) {
                finalFacets[k].push({'key':kk,'value':facetsSoFar[k][kk].length});
              }
              if (q.facetSort) {
                if (q.facetSort == 'keyAsc') finalFacets[k].sort(sortOnKeyAsc);
                else if (q.facetSort == 'keyDesc') finalFacets[k].sort(sortOnKeyDesc);
                else if (q.facetSort == 'valueAsc') finalFacets[k].sort(sortOnValueAsc);
                else if (q.facetSort == 'valueDesc') finalFacets[k].sort(sortOnValueDesc);
              }
              else {
                finalFacets[k].sort(sortOnValueAsc);
              }
              finalFacets[k] = finalFacets[k].slice(0, facetLength);
            }
            //finished!
            callback(finalFacets);
          }
        });
    }
    getFacetSets(facetKeySet, 0);
  };

  //remove stopwords
  cleanQuery['query'] = {};
  for (var searchField in q['query']) {
    var cleanQueryFieldToken = searchField;
    cleanQuery['query'][cleanQueryFieldToken] = [];
    for (var k = 0; k < q['query'][searchField].length; k++) {
      cleanQuery['query'][cleanQueryFieldToken].push(q['query'][searchField][k]);
    }
    //TODO: does this make sense?
    if (cleanQuery['query'][cleanQueryFieldToken].length == 0) {
      canSearch = false;
    }
  }

  if (q.weight) {
    cleanQuery['weight'] = q.weight;
  }

  logger.debug('cleanQuery: ' + JSON.stringify(cleanQuery));

  var filterKeySet = [];
  for (k in q.filter) {
    for (var i = 0; i < q.filter[k].length; i++) {
      filterKeySet.push(k + '~' + q.filter[k][i]);
    }
  }

  //generate keyset
  var RIKeySet = [];
  for (var queryField in cleanQuery.query) {
    for (var j = 0; j < cleanQuery.query[queryField].length; j++) {
      if (q.filter) {
        for (k in q.filter) {
//          logger.debug('filter[k]: ' + q.filter[k]);
          for (var i = 0; i < q.filter[k].length; i++) {
            keySet.push('TF~' + queryField + '~' + cleanQuery.query[queryField][j] + '~'
                        + k + '~' + q.filter[k][i]);
            RIKeySet.push('RI~' + queryField + '~' + cleanQuery.query[queryField][j] + '~'
                          + k + '~' + q.filter[k][i]);
          }
        }
      }
      else {
        keySet.push('TF~' + queryField + '~' + cleanQuery.query[queryField][j] + '~~');
        RIKeySet.push('RI~' + queryField + '~' + cleanQuery.query[queryField][j] + '~~');
      }
    }
  }

  var getEmptyResultSet = function() {
    var resultSet = {};
    resultSet['hits'] = [];
    return resultSet;
  }

  getDocumentFreqencies(indexesMultiply, function(err, frequencies) {
    if (err) return callback(getEmptyResultSet());
    getFacets(q, indexes, function(facets) {
//      console.log('facets: ' + JSON.stringify(facets))
      var response = {};
      response['totalDocs'] = frequencies.allDocsIDsInResultSet.length;
      response['facets'] = facets;
      response['hits'] = [];

      console.log(RIKeySet);

      indexesMultiply.get(RIKeySet, function(err, data) {
        //console.log(data);
        //Dump all hits with all terms in unsorted object
        var unsortedHits = {};
        for (var k in data) {
          //account for weighting
          var fieldWeight = 1;
          var fieldName = k.split('~')[1];
          if (q.weight)
            if (q.weight[fieldName])
              fieldWeight = q.weight[fieldName];

          //How far down the frequency set you want to look.
          //Bigger == more accurate, smaller == faster.
          var seekCutoff = 100;

          for (var i = 0; ((i < data[k].length) && (i < seekCutoff)); i++) {
            if (frequencies.allDocsIDsInResultSet.indexOf(data[k][i][1]) != -1) {
              var tuple = [k, (fieldWeight*data[k][i][0])];
              try {
                unsortedHits[data[k][i][1]].push(tuple);
              }
              catch (err) {
                unsortedHits[data[k][i][1]] = [tuple];
              }
            }
          }
        }

        //Iterate through object, calculating scores, account for weighting
        var sortedHits = [];
        for (var k in unsortedHits) {
          var score = 0;
          for (var i = 0; i < unsortedHits[k].length; i++)
            score += unsortedHits[k][i][1];
          sortedHits.push([k, score]);
        }
        sortedHits = sortedHits.sort(function(a,b) {
          if (a[1]  b[1])
            return -1;
          return 0;
        });

        //set offset and pagesize
        var offset = 0;
        if (q.offset) offset = q.offset;
        var pageSize = 100;
        if (q.pageSize) pageSize = q.pageSize;
        sortedHits = sortedHits.slice(offset, (offset + pageSize));

        var IDsToBeGlued = [];
        var IDsToBeGluedWithScores = {};
        //get ids of results to glue
        for (var i = 0; i < sortedHits.length; i++) {
          IDsToBeGlued.push('DOCUMENT~' + sortedHits[i][0] + '~');
          IDsToBeGluedWithScores['DOCUMENT~' + sortedHits[i][0] + '~'] = sortedHits[i][1];
        }

        //Glue docs to IDs and send resultset

        indexesMultiply.get(IDsToBeGlued, function(err, data) {
          for (var k in data) {
            var hit = {};
            hit['id'] = k.split('~')[1];
            hit['score'] = IDsToBeGluedWithScores[k];
            hit['document'] = JSON.parse(data[k]);
            var terms = q['query']['*'];
            if (q['query'][q.teaser])
              terms = q['query'][q.teaser];
            if (q.teaser && hit.document[q.teaser]) {
              try {
                hit.document['teaser'] =
                  scontext(hit.document[q.teaser],
                           terms, 400,
                           function hi (string) {
                             return '' + string + ''
                           });
              } catch (e) {
                console.log('error with teaser generation: ' + e)
              }
            }
            delete hit['document']['*'];
            response["hits"].push(hit);
          }

          response['hits'] = response.hits.sort(function(a,b) {
            if (a.score  b.score)
              return -1;
            return 0;
          });
          response['totalHits'] = frequencies.allDocsIDsInResultSet.length;
          response['query'] = q;

          callback(response);

        });
      });
    });
  });
};

function intersectionDestructive(a, b) {
  var result = new Array();
  while( a.length > 0 && b.length > 0 ) {
     if      (a[0]  b[0]){ b.shift(); }
     else /* they're equal */ {
       result.push(a.shift());
       b.shift();
     }
  }
  return result;
}
<commit_msg>seekCutOff kicks in automatically if there is only one search term<commit_after>var stopwords = require('natural').stopwords;
var mapreduce = require('./mapreduce.js');
var logger = require('../logger.js');
var scontext = require('search-context');
var totalDocs = 0;

//used on startup
exports.setTotalDocs = function (td) {
  totalDocs = td;
}

exports.search = function (indexes, indexesMultiply, q, callback) {
  var cleanQuery = {};
  var canSearch = true;
  var keySet = [];

  var getDocumentFreqencies = function(indexesMultiply, callback) {
    //Check document frequencies
    var err = false;
    var docFreqs = {};
    var tfsets = [];
    var totalHits = 0;
    console.log('HEEEEE')
    indexesMultiply.get(keySet, function(err, data) {
      for (k in data) {
        if (data[k] == null) {
          //return a zero result
          docFreqs[keySet[i]] = 0;
          logger.debug('KEY ' + k + ' NOT FOUND IN DICTIONARY');
        }
        else {
      //    totalHits = data[k].length;
          docFreqs[k] = data[k].length;
          tfsets.push(data[k]);
        }
      }
      var intersection = tfsets[0];
      for (var i = 1; i < tfsets.length; i++) {
        intersection = intersectionDestructive(intersection, tfsets[i]);
      }

      if(intersection === undefined)
        err = true;
      else
        totalHits = intersection.length;

      //    console.log('tntersection: ' + intersection);
      //    pickSearch(docFreqs);
      callback(err, {docFreqs:docFreqs, allDocsIDsInResultSet:intersection});
    });
  };

  var getFacets = function(q, indexes, callback) {
    //if no facet request callback and return
    if (!q.facets){callback({});return;}

    var facetLength = 10;
    if (q.facetLength) facetLength = q.facetLength;

    var sortOnValueDesc = function(a,b) {
      if (a.value  b.value)
        return -1;
      return 0;
    }
    var sortOnValueAsc = function(a,b) {
      if (a.value  b.value)
        return 1;
      return 0;
    }
    var sortOnKeyDesc = function(a,b) {
      if (a.key  b.key)
        return -1;
      return 0;
    }
    var sortOnKeyAsc = function(a,b) {
      if (a.key  b.key)
        return 1;
      return 0;
    }

    var facetKeySet = [];
    for (var searchField in q.query) {
      for (var h = 0; h < q.query[searchField].length; h++) {
        var queryToken = q.query[searchField][h];
        for (var i = 0; i < q.facets.length; i++) {
          facetKeySet.push('TF~' + searchField + '~' + queryToken + '~' + q.facets[i]);
        }
      }
    }

    var facetsSoFar = {};
    for (var i = 0; i < q.facets.length; i++)
      facetsSoFar[q.facets[i]] = {};

    //could be speeded up by always taking the least frequent keys first?
    var getFacetSets = function(fks, counter) {

      var sortOnValueDesc = function(a,b) {
        if (a.value  b.value)
          return -1;
        return 0;
      }
      var sortOnValueAsc = function(a,b) {
        if (a.value  b.value)
          return 1;
        return 0;
      }
      var sortOnKeyDesc = function(a,b) {
        if (a.key  b.key)
          return -1;
        return 0;
      }
      var sortOnKeyAsc = function(a,b) {
        if (a.key  b.key)
          return 1;
        return 0;
      }

      var thisFacetCat = fks[counter].split('~')[3];
      indexes.createReadStream({
        valueEncoding: 'json',
        limit: -1,
        start: fks[counter] + '~',
        end: fks[counter] + '~~'})
        .on('data', function (data) {
          var thisFacetName = data.key.split('~')[4];
          if (!facetsSoFar[thisFacetCat][thisFacetName]) {
            facetsSoFar[thisFacetCat][thisFacetName] = data.value;
          }
          else {
            var intersection = intersectionDestructive(facetsSoFar[thisFacetCat][thisFacetName], data.value);
            if (intersection.length > 0)
              facetsSoFar[thisFacetCat][thisFacetName] = intersection;
            else
              delete facetsSoFar[thisFacetCat][thisFacetName];
          }
        })
        .on('end', function () {
          if (++counter < fks.length) {getFacetSets(facetKeySet, counter);}
          else {
            //flatten sets to counts
            var finalFacets = {};
            for (k in facetsSoFar) {
              finalFacets[k] = [];
              for (kk in facetsSoFar[k]) {
                finalFacets[k].push({'key':kk,'value':facetsSoFar[k][kk].length});
              }
              if (q.facetSort) {
                if (q.facetSort == 'keyAsc') finalFacets[k].sort(sortOnKeyAsc);
                else if (q.facetSort == 'keyDesc') finalFacets[k].sort(sortOnKeyDesc);
                else if (q.facetSort == 'valueAsc') finalFacets[k].sort(sortOnValueAsc);
                else if (q.facetSort == 'valueDesc') finalFacets[k].sort(sortOnValueDesc);
              }
              else {
                finalFacets[k].sort(sortOnValueAsc);
              }
              finalFacets[k] = finalFacets[k].slice(0, facetLength);
            }
            //finished!
            callback(finalFacets);
          }
        });
    }
    getFacetSets(facetKeySet, 0);
  };

  //remove stopwords
  cleanQuery['query'] = {};
  for (var searchField in q['query']) {
    var cleanQueryFieldToken = searchField;
    cleanQuery['query'][cleanQueryFieldToken] = [];
    for (var k = 0; k < q['query'][searchField].length; k++) {
      cleanQuery['query'][cleanQueryFieldToken].push(q['query'][searchField][k]);
    }
    //TODO: does this make sense?
    if (cleanQuery['query'][cleanQueryFieldToken].length == 0) {
      canSearch = false;
    }
  }

  if (q.weight) {
    cleanQuery['weight'] = q.weight;
  }

  logger.debug('cleanQuery: ' + JSON.stringify(cleanQuery));

  var filterKeySet = [];
  for (k in q.filter) {
    for (var i = 0; i < q.filter[k].length; i++) {
      filterKeySet.push(k + '~' + q.filter[k][i]);
    }
  }

  //generate keyset
  var RIKeySet = [];
  for (var queryField in cleanQuery.query) {
    for (var j = 0; j < cleanQuery.query[queryField].length; j++) {
      if (q.filter) {
        for (k in q.filter) {
//          logger.debug('filter[k]: ' + q.filter[k]);
          for (var i = 0; i < q.filter[k].length; i++) {
            keySet.push('TF~' + queryField + '~' + cleanQuery.query[queryField][j] + '~'
                        + k + '~' + q.filter[k][i]);
            RIKeySet.push('RI~' + queryField + '~' + cleanQuery.query[queryField][j] + '~'
                          + k + '~' + q.filter[k][i]);
          }
        }
      }
      else {
        keySet.push('TF~' + queryField + '~' + cleanQuery.query[queryField][j] + '~~');
        RIKeySet.push('RI~' + queryField + '~' + cleanQuery.query[queryField][j] + '~~');
      }
    }
  }

  var getEmptyResultSet = function() {
    var resultSet = {};
    resultSet['hits'] = [];
    return resultSet;
  }

  getDocumentFreqencies(indexesMultiply, function(err, frequencies) {
    if (err) return callback(getEmptyResultSet());
    getFacets(q, indexes, function(facets) {
//      console.log('facets: ' + JSON.stringify(facets))
      var response = {};
      response['totalDocs'] = frequencies.allDocsIDsInResultSet.length;
      response['facets'] = facets;
      response['hits'] = [];

      console.log(RIKeySet);

      indexesMultiply.get(RIKeySet, function(err, data) {
        //console.log(data);
        //Dump all hits with all terms in unsorted object
        var unsortedHits = {};
        for (var k in data) {
          //account for weighting
          var fieldWeight = 1;
          var fieldName = k.split('~')[1];
          if (q.weight)
            if (q.weight[fieldName])
              fieldWeight = q.weight[fieldName];

          //How far down the frequency set you want to look.
          //Bigger == more accurate, smaller == faster.
          var seekCutOff = 50000;

          //if there is only one key in the set, and only one query term
          //set cutoff to be the same length as the page
          if ((q.pageSize) && (RIKeySet.length == 1)) seekCutOff = q.pageSize;

          for (var i = 0; ((i < data[k].length) && (i < seekCutOff)); i++) {
            if (frequencies.allDocsIDsInResultSet.indexOf(data[k][i][1]) != -1) {
              var tuple = [k, (fieldWeight*data[k][i][0])];
              try {
                unsortedHits[data[k][i][1]].push(tuple);
              }
              catch (err) {
                unsortedHits[data[k][i][1]] = [tuple];
              }
            }
          }
        }

        //Iterate through object, calculating scores, account for weighting
        var sortedHits = [];
        for (var k in unsortedHits) {
          var score = 0;
          for (var i = 0; i < unsortedHits[k].length; i++)
            score += unsortedHits[k][i][1];
          sortedHits.push([k, score]);
        }
        sortedHits = sortedHits.sort(function(a,b) {
          if (a[1]  b[1])
            return -1;
          return 0;
        });

        //set offset and pagesize
        var offset = 0;
        if (q.offset) offset = q.offset;
        var pageSize = 100;
        if (q.pageSize) pageSize = q.pageSize;
        sortedHits = sortedHits.slice(offset, (offset + pageSize));

        var IDsToBeGlued = [];
        var IDsToBeGluedWithScores = {};
        //get ids of results to glue
        for (var i = 0; i < sortedHits.length; i++) {
          IDsToBeGlued.push('DOCUMENT~' + sortedHits[i][0] + '~');
          IDsToBeGluedWithScores['DOCUMENT~' + sortedHits[i][0] + '~'] = sortedHits[i][1];
        }

        //Glue docs to IDs and send resultset

        indexesMultiply.get(IDsToBeGlued, function(err, data) {
          for (var k in data) {
            var hit = {};
            hit['id'] = k.split('~')[1];
            hit['score'] = IDsToBeGluedWithScores[k];
            hit['document'] = JSON.parse(data[k]);
            var terms = q['query']['*'];
            if (q['query'][q.teaser])
              terms = q['query'][q.teaser];
            if (q.teaser && hit.document[q.teaser]) {
              try {
                hit.document['teaser'] =
                  scontext(hit.document[q.teaser],
                           terms, 400,
                           function hi (string) {
                             return '' + string + ''
                           });
              } catch (e) {
                console.log('error with teaser generation: ' + e)
              }
            }
            delete hit['document']['*'];
            response["hits"].push(hit);
          }

          response['hits'] = response.hits.sort(function(a,b) {
            if (a.score  b.score)
              return -1;
            return 0;
          });
          response['totalHits'] = frequencies.allDocsIDsInResultSet.length;
          response['query'] = q;

          callback(response);

        });
      });
    });
  });
};

function intersectionDestructive(a, b) {
  var result = new Array();
  while( a.length > 0 && b.length > 0 ) {
     if      (a[0]  b[0]){ b.shift(); }
     else /* they're equal */ {
       result.push(a.shift());
       b.shift();
     }
  }
  return result;
}
<|endoftext|>