<commit_before>const DEFAULT_PLACEMENT = 'point-sample';

/**
 * Returns a template function (function that accepts template parameters and returns a string)
 * to generate an aggregation query.
 * Valid options to define the query template are:
 * - placement
 * - columns
 * - dimensions*
 * The query template parameters taken by the result template function are:
 * - sourceQuery
 * - res
 * - columns
 * - dimensions
 */
const templateForOptions = (options) => {
    let templateFn = defaultAggregationQueryTemplate;
    if (!options.isDefaultAggregation) {
        templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
        if (!templateFn) {
            throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
        }
    }
    return templateFn;
};

/**
 * Generates an aggregation query given the aggregation options:
 * - query
 * - resolution - defined as in torque:
 *   aggregation cell is resolution*resolution pixels, where tiles are always 256x256 pixels
 * - columns
 * - placement
 * - dimensions
 *
 * The default aggregation (when no explicit placement, columns or dimensions are present) returns
 * a sample record (with all the original columns and _cdb_feature_count) for each aggregation group.
 * When placement, columns or dimensions are specified, columns are aggregated as requested
 * (by default only _cdb_feature_count) and with the_geom_webmercator as defined by placement.
 */
const queryForOptions = (options) => templateForOptions(options)({
    sourceQuery: options.query,
    res: 256/options.resolution,
    columns: options.columns,
    dimensions: options.dimensions,
    filters: options.filters
});

module.exports = queryForOptions;

const SUPPORTED_AGGREGATE_FUNCTIONS = {
    'count': {
        sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
    },
    'avg': {
        sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
    },
    'sum': {
        sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
    },
    'min': {
        sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
    },
    'max': {
        sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
    },
    'mode': {
        sql: (column_name, params) => `_cdb_mode(${params.aggregated_column || column_name})`
    }
};

module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);

const sep = (list) => {
    let expr = list.join(', ');
    return expr ? ', ' + expr : expr;
};

const aggregateColumns = ctx => {
    return Object.assign({
        _cdb_feature_count: {
            aggregate_function: 'count'
        }
    }, ctx.columns || {});
};

const aggregateColumnNames = (ctx, table) => {
    let columns = aggregateColumns(ctx);
    if (table) {
        return sep(Object.keys(columns).map(
            column_name => `${table}.${column_name}`
        ));
    }
    return sep(Object.keys(columns));
};

const aggregateExpression = (column_name, column_parameters) => {
    const aggregate_function = column_parameters.aggregate_function || 'count';
    const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
    if (!aggregate_definition) {
        throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
    }
    return aggregate_definition.sql(column_name, column_parameters);
};

const aggregateColumnDefs = ctx => {
    let columns = aggregateColumns(ctx);
    return sep(Object.keys(columns).map(column_name => {
        const aggregate_expression = aggregateExpression(column_name, columns[column_name]);
        return `${aggregate_expression} AS ${column_name}`;
    }));
};

const aggregateDimensions = ctx => ctx.dimensions || {};

const dimensionNames = (ctx, table) => {
    let dimensions = aggregateDimensions(ctx);
    if (table) {
        return sep(Object.keys(dimensions).map(
            dimension_name => `${table}.${dimension_name}`
        ));
    }
    return sep(Object.keys(dimensions));
};

const dimensionDefs = ctx => {
    let dimensions = aggregateDimensions(ctx);
    return sep(Object.keys(dimensions).map(dimension_name => {
        const expression = dimensions[dimension_name];
        return `${expression} AS ${dimension_name}`;
    }));
};

const aggregateFilters = ctx => ctx.filters || {};

const filterConditionSQL = (expr, filter) => {
    // TODO: validate filter parameters (e.g. cannot have both greater_than and greater_than or equal to)

    if (filter) {
        if (!Array.isArray(filter)) {
            filter = [filter];
        }
        if (filter.length > 0) {
            return filter.map(f => filterSingleConditionSQL(expr, f)).join(' OR ');
        }
    }
};

const filterSingleConditionSQL = (expr, filter) => {
    let cond;
    Object.keys(FILTERS).some(f => {
        cond = FILTERS[f](expr, filter);
        return cond;
    });
    return cond;
};

const sqlQ = (value) => {
    if (isFinite(value)) {
        return String(value);
    }
    return `'${value}'`; // TODO: escape single quotes! (by doubling them)
};

/* jshint eqeqeq: false */
/* x != null is used to check for both null and undefined; triple !== wouldn't do the trick */

const FILTERS = {
    between: (expr, filter) => {
        const lo = filter.greater_than_or_equal_to, hi = filter.less_than_or_equal_to;
        if (lo != null && hi != null) {
            return `(${expr} BETWEEN ${sqlQ(lo)} AND ${sqlQ(hi)})`;
        }
    },
    in: (expr, filter) => {
        if (filter.in != null) {
            return `(${expr} IN (${filter.in.map(v => sqlQ(v)).join(',')}))`;
        }
    },
    notin: (expr, filter) => {
        if (filter.not_in != null) {
            return `(${expr} NOT IN (${filter.not_in.map(v => sqlQ(v)).join(',')}))`;
        }
    },
    equal: (expr, filter) => {
        if (filter.equal != null) {
            return `(${expr} = ${sqlQ(filter.equal)})`;
        }
    },
    not_equal: (expr, filter) => {
        if (filter.not_equal != null) {
            return `(${expr} <> ${sqlQ(filter.not_equal)})`;
        }
    },
    range: (expr, filter) => {
        let conds = [];
        if (filter.greater_than_or_equal_to != null) {
            conds.push(`(${expr} >= ${sqlQ(filter.greater_than_or_equal_to)})`);
        }
        if (filter.greater_than != null) {
            conds.push(`(${expr} > ${sqlQ(filter.greater_than)})`);
        }
        if (filter.less_than_or_equal_to != null) {
            conds.push(`(${expr} <= ${sqlQ(filter.less_than_or_equal_to)})`);
        }
        if (filter.less_than != null) {
            conds.push(`(${expr} < ${sqlQ(filter.less_than)})`);
        }
        if (conds.length > 0) {
            return conds.join(' AND ');
        }
    }
};

const filterConditions = ctx => {
    let columns = aggregateColumns(ctx);
    let dimensions = aggregateDimensions(ctx);
    let filters = aggregateFilters(ctx);
    return Object.keys(filters).map(filtered_column => {
        let filtered_expr;
        if (columns[filtered_column]) {
            filtered_expr = aggregateExpression(filtered_column, columns[filtered_column]);
        }
        else if (dimensions[filtered_column]) {
            filtered_expr = dimensions[filtered_column];
        }
        if (!filtered_expr) {
            throw new Error("Invalid filtered column: '" + filtered_column + "'");
        }
        return filterConditionSQL(filtered_expr, filters[filtered_column]);
    }).join(' AND ');
};

const havingClause = ctx => {
    let cond = filterConditions(ctx);
    return cond ? `HAVING ${cond}` : '';
};

// SQL expression to compute the aggregation resolution (grid cell size).
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
// (i.e. each tile is divided into ctx.res*ctx.res cells).
// We limit the the minimum resolution to avoid division by zero problems. The limit used is
// the pixel size of zoom level 30 (i.e. 1/2*(30+8) of the full earth web-mercator extent), which is about 0.15 mm.
const gridResolution = ctx => {
    const minimumResolution = 2*Math.PI*6378137/Math.pow(2,38);
    return `GREATEST(${256*0.00028/ctx.res}*!scale_denominator!, ${minimumResolution})::double precision`;
};

// Notes:
// * We need to filter spatially using !bbox! to make the queries efficient because
//   the filter added by Mapnik (wrapping the query)
//   is only applied after the aggregation.
// * This queries are used for rendering and the_geom is omitted in the results for better performance

// The special default aggregation includes all the columns of a sample row per grid cell and
// the count (_cdb_feature_count) of the aggregated rows.
const defaultAggregationQueryTemplate = ctx => `
    WITH
    _cdb_params AS (
        SELECT
        ${gridResolution(ctx)} AS res,
        !bbox! AS bbox
    ),
    _cdb_clusters AS (
        SELECT
            MIN(cartodb_id) AS cartodb_id
            ${dimensionDefs(ctx)}
            ${aggregateColumnDefs(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
        GROUP BY
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
            ${dimensionNames(ctx)}
        ${havingClause(ctx)}
    ) SELECT
        _cdb_query.*
        ${aggregateColumnNames(ctx)}
    FROM
        _cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
        ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
`;

const aggregationQueryTemplates = {
    'centroid': ctx => `
        WITH
        _cdb_params AS (
            SELECT
            ${gridResolution(ctx)} AS res,
            !bbox! AS bbox
        )
        SELECT
            row_number() over() AS cartodb_id,
            ST_SetSRID(
                ST_MakePoint(
                    AVG(ST_X(_cdb_query.the_geom_webmercator)),
                    AVG(ST_Y(_cdb_query.the_geom_webmercator))
                ), 3857
            ) AS the_geom_webmercator
            ${dimensionDefs(ctx)}
            ${aggregateColumnDefs(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
        GROUP BY
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
            ${dimensionNames(ctx)}
        ${havingClause(ctx)}
    `,

    'point-grid': ctx => `
        WITH
        _cdb_params AS (
            SELECT
            ${gridResolution(ctx)} AS res,
            !bbox! AS bbox
        ),
        _cdb_clusters AS (
            SELECT
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
            ${dimensionDefs(ctx)}
            ${aggregateColumnDefs(ctx)}
            FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
            WHERE the_geom_webmercator && _cdb_params.bbox
            GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
            ${havingClause(ctx)}
        )
        SELECT
            row_number() over() AS cartodb_id,
            ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
            ${dimensionNames(ctx)}
            ${aggregateColumnNames(ctx)}
            FROM _cdb_clusters, _cdb_params
    `,

    'point-sample': ctx => `
        WITH
        _cdb_params AS (
            SELECT
            ${gridResolution(ctx)} AS res,
            !bbox! AS bbox
        ),
        _cdb_clusters AS (
            SELECT
                MIN(cartodb_id) AS cartodb_id
                ${dimensionDefs(ctx)}
                ${aggregateColumnDefs(ctx)}
            FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
            WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
            GROUP BY
                Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
                Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
                ${dimensionNames(ctx)}
            ${havingClause(ctx)}
        )
        SELECT
            _cdb_clusters.cartodb_id,
            the_geom, the_geom_webmercator
            ${dimensionNames(ctx, '_cdb_query')}
            ${aggregateColumnNames(ctx, '_cdb_clusters')}
        FROM
            _cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
            ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
    `

};

module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
module.exports.GEOMETRY_COLUMN = 'the_geom_webmercator';
<commit_msg>Remove superfluous aggregation filter condition<commit_after>const DEFAULT_PLACEMENT = 'point-sample';

/**
 * Returns a template function (function that accepts template parameters and returns a string)
 * to generate an aggregation query.
 * Valid options to define the query template are:
 * - placement
 * - columns
 * - dimensions*
 * The query template parameters taken by the result template function are:
 * - sourceQuery
 * - res
 * - columns
 * - dimensions
 */
const templateForOptions = (options) => {
    let templateFn = defaultAggregationQueryTemplate;
    if (!options.isDefaultAggregation) {
        templateFn = aggregationQueryTemplates[options.placement || DEFAULT_PLACEMENT];
        if (!templateFn) {
            throw new Error("Invalid Aggregation placement: '" + options.placement + "'");
        }
    }
    return templateFn;
};

/**
 * Generates an aggregation query given the aggregation options:
 * - query
 * - resolution - defined as in torque:
 *   aggregation cell is resolution*resolution pixels, where tiles are always 256x256 pixels
 * - columns
 * - placement
 * - dimensions
 *
 * The default aggregation (when no explicit placement, columns or dimensions are present) returns
 * a sample record (with all the original columns and _cdb_feature_count) for each aggregation group.
 * When placement, columns or dimensions are specified, columns are aggregated as requested
 * (by default only _cdb_feature_count) and with the_geom_webmercator as defined by placement.
 */
const queryForOptions = (options) => templateForOptions(options)({
    sourceQuery: options.query,
    res: 256/options.resolution,
    columns: options.columns,
    dimensions: options.dimensions,
    filters: options.filters
});

module.exports = queryForOptions;

const SUPPORTED_AGGREGATE_FUNCTIONS = {
    'count': {
        sql: (column_name, params) => `count(${params.aggregated_column || '*'})`
    },
    'avg': {
        sql: (column_name, params) => `avg(${params.aggregated_column || column_name})`
    },
    'sum': {
        sql: (column_name, params) => `sum(${params.aggregated_column || column_name})`
    },
    'min': {
        sql: (column_name, params) => `min(${params.aggregated_column || column_name})`
    },
    'max': {
        sql: (column_name, params) => `max(${params.aggregated_column || column_name})`
    },
    'mode': {
        sql: (column_name, params) => `_cdb_mode(${params.aggregated_column || column_name})`
    }
};

module.exports.SUPPORTED_AGGREGATE_FUNCTIONS = Object.keys(SUPPORTED_AGGREGATE_FUNCTIONS);

const sep = (list) => {
    let expr = list.join(', ');
    return expr ? ', ' + expr : expr;
};

const aggregateColumns = ctx => {
    return Object.assign({
        _cdb_feature_count: {
            aggregate_function: 'count'
        }
    }, ctx.columns || {});
};

const aggregateColumnNames = (ctx, table) => {
    let columns = aggregateColumns(ctx);
    if (table) {
        return sep(Object.keys(columns).map(
            column_name => `${table}.${column_name}`
        ));
    }
    return sep(Object.keys(columns));
};

const aggregateExpression = (column_name, column_parameters) => {
    const aggregate_function = column_parameters.aggregate_function || 'count';
    const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
    if (!aggregate_definition) {
        throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
    }
    return aggregate_definition.sql(column_name, column_parameters);
};

const aggregateColumnDefs = ctx => {
    let columns = aggregateColumns(ctx);
    return sep(Object.keys(columns).map(column_name => {
        const aggregate_expression = aggregateExpression(column_name, columns[column_name]);
        return `${aggregate_expression} AS ${column_name}`;
    }));
};

const aggregateDimensions = ctx => ctx.dimensions || {};

const dimensionNames = (ctx, table) => {
    let dimensions = aggregateDimensions(ctx);
    if (table) {
        return sep(Object.keys(dimensions).map(
            dimension_name => `${table}.${dimension_name}`
        ));
    }
    return sep(Object.keys(dimensions));
};

const dimensionDefs = ctx => {
    let dimensions = aggregateDimensions(ctx);
    return sep(Object.keys(dimensions).map(dimension_name => {
        const expression = dimensions[dimension_name];
        return `${expression} AS ${dimension_name}`;
    }));
};

const aggregateFilters = ctx => ctx.filters || {};

const filterConditionSQL = (expr, filter) => {
    // TODO: validate filter parameters (e.g. cannot have both greater_than and greater_than or equal to)

    if (filter) {
        if (!Array.isArray(filter)) {
            filter = [filter];
        }
        if (filter.length > 0) {
            return filter.map(f => filterSingleConditionSQL(expr, f)).join(' OR ');
        }
    }
};

const filterSingleConditionSQL = (expr, filter) => {
    let cond;
    Object.keys(FILTERS).some(f => {
        cond = FILTERS[f](expr, filter);
        return cond;
    });
    return cond;
};

const sqlQ = (value) => {
    if (isFinite(value)) {
        return String(value);
    }
    return `'${value}'`; // TODO: escape single quotes! (by doubling them)
};

/* jshint eqeqeq: false */
/* x != null is used to check for both null and undefined; triple !== wouldn't do the trick */

const FILTERS = {
    between: (expr, filter) => {
        const lo = filter.greater_than_or_equal_to, hi = filter.less_than_or_equal_to;
        if (lo != null && hi != null) {
            return `(${expr} BETWEEN ${sqlQ(lo)} AND ${sqlQ(hi)})`;
        }
    },
    in: (expr, filter) => {
        if (filter.in != null) {
            return `(${expr} IN (${filter.in.map(v => sqlQ(v)).join(',')}))`;
        }
    },
    notin: (expr, filter) => {
        if (filter.not_in != null) {
            return `(${expr} NOT IN (${filter.not_in.map(v => sqlQ(v)).join(',')}))`;
        }
    },
    equal: (expr, filter) => {
        if (filter.equal != null) {
            return `(${expr} = ${sqlQ(filter.equal)})`;
        }
    },
    not_equal: (expr, filter) => {
        if (filter.not_equal != null) {
            return `(${expr} <> ${sqlQ(filter.not_equal)})`;
        }
    },
    range: (expr, filter) => {
        let conds = [];
        if (filter.greater_than_or_equal_to != null) {
            conds.push(`(${expr} >= ${sqlQ(filter.greater_than_or_equal_to)})`);
        }
        if (filter.greater_than != null) {
            conds.push(`(${expr} > ${sqlQ(filter.greater_than)})`);
        }
        if (filter.less_than_or_equal_to != null) {
            conds.push(`(${expr} <= ${sqlQ(filter.less_than_or_equal_to)})`);
        }
        if (filter.less_than != null) {
            conds.push(`(${expr} < ${sqlQ(filter.less_than)})`);
        }
        if (conds.length > 0) {
            return conds.join(' AND ');
        }
    }
};

const filterConditions = ctx => {
    let columns = aggregateColumns(ctx);
    let dimensions = aggregateDimensions(ctx);
    let filters = aggregateFilters(ctx);
    return Object.keys(filters).map(filtered_column => {
        let filtered_expr;
        if (columns[filtered_column]) {
            filtered_expr = aggregateExpression(filtered_column, columns[filtered_column]);
        }
        else if (dimensions[filtered_column]) {
            filtered_expr = dimensions[filtered_column];
        }
        if (!filtered_expr) {
            throw new Error("Invalid filtered column: '" + filtered_column + "'");
        }
        return filterConditionSQL(filtered_expr, filters[filtered_column]);
    }).join(' AND ');
};

const havingClause = ctx => {
    let cond = filterConditions(ctx);
    return cond ? `HAVING ${cond}` : '';
};

// SQL expression to compute the aggregation resolution (grid cell size).
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
// (i.e. each tile is divided into ctx.res*ctx.res cells).
// We limit the the minimum resolution to avoid division by zero problems. The limit used is
// the pixel size of zoom level 30 (i.e. 1/2*(30+8) of the full earth web-mercator extent), which is about 0.15 mm.
const gridResolution = ctx => {
    const minimumResolution = 2*Math.PI*6378137/Math.pow(2,38);
    return `GREATEST(${256*0.00028/ctx.res}*!scale_denominator!, ${minimumResolution})::double precision`;
};

// Notes:
// * We need to filter spatially using !bbox! to make the queries efficient because
//   the filter added by Mapnik (wrapping the query)
//   is only applied after the aggregation.
// * This queries are used for rendering and the_geom is omitted in the results for better performance

// The special default aggregation includes all the columns of a sample row per grid cell and
// the count (_cdb_feature_count) of the aggregated rows.
const defaultAggregationQueryTemplate = ctx => `
    WITH
    _cdb_params AS (
        SELECT
        ${gridResolution(ctx)} AS res,
        !bbox! AS bbox
    ),
    _cdb_clusters AS (
        SELECT
            MIN(cartodb_id) AS cartodb_id
            ${dimensionDefs(ctx)}
            ${aggregateColumnDefs(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
        GROUP BY
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
            ${dimensionNames(ctx)}
    ) SELECT
        _cdb_query.*
        ${aggregateColumnNames(ctx)}
    FROM
        _cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
        ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
`;

const aggregationQueryTemplates = {
    'centroid': ctx => `
        WITH
        _cdb_params AS (
            SELECT
            ${gridResolution(ctx)} AS res,
            !bbox! AS bbox
        )
        SELECT
            row_number() over() AS cartodb_id,
            ST_SetSRID(
                ST_MakePoint(
                    AVG(ST_X(_cdb_query.the_geom_webmercator)),
                    AVG(ST_Y(_cdb_query.the_geom_webmercator))
                ), 3857
            ) AS the_geom_webmercator
            ${dimensionDefs(ctx)}
            ${aggregateColumnDefs(ctx)}
        FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
        WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
        GROUP BY
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
            ${dimensionNames(ctx)}
        ${havingClause(ctx)}
    `,

    'point-grid': ctx => `
        WITH
        _cdb_params AS (
            SELECT
            ${gridResolution(ctx)} AS res,
            !bbox! AS bbox
        ),
        _cdb_clusters AS (
            SELECT
            Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
            Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
            ${dimensionDefs(ctx)}
            ${aggregateColumnDefs(ctx)}
            FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
            WHERE the_geom_webmercator && _cdb_params.bbox
            GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
            ${havingClause(ctx)}
        )
        SELECT
            row_number() over() AS cartodb_id,
            ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
            ${dimensionNames(ctx)}
            ${aggregateColumnNames(ctx)}
            FROM _cdb_clusters, _cdb_params
    `,

    'point-sample': ctx => `
        WITH
        _cdb_params AS (
            SELECT
            ${gridResolution(ctx)} AS res,
            !bbox! AS bbox
        ),
        _cdb_clusters AS (
            SELECT
                MIN(cartodb_id) AS cartodb_id
                ${dimensionDefs(ctx)}
                ${aggregateColumnDefs(ctx)}
            FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
            WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
            GROUP BY
                Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
                Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
                ${dimensionNames(ctx)}
            ${havingClause(ctx)}
        )
        SELECT
            _cdb_clusters.cartodb_id,
            the_geom, the_geom_webmercator
            ${dimensionNames(ctx, '_cdb_query')}
            ${aggregateColumnNames(ctx, '_cdb_clusters')}
        FROM
            _cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
            ON (_cdb_clusters.cartodb_id = _cdb_query.cartodb_id)
    `

};

module.exports.SUPPORTED_PLACEMENTS = Object.keys(aggregationQueryTemplates);
module.exports.GEOMETRY_COLUMN = 'the_geom_webmercator';
<|endoftext|>


<commit_before>// ==UserScript==
// @name         xDiep.io (github)
// @description  Diep.io extension (sectored-minimap, zoom, play-with-friends, custom-theming, kill counter)
// @version      3.0
// @author       MungBo
// @match        http://diep.io/
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @connect      diep.io
// @namespace    https://raw.githubusercontent.com/happyhg3927/Diep.io/master/xdiep.user.js
// ==/UserScript==

window.stop();

GM_xmlhttpRequest({
    method: "GET",
    url: "http://diep.io",
    onload: function(e) {
         document.open(), document.write("<script src='https://cdn.rawgit.com/happyhg3927/Diep.io/master/head.js'></script>" + e.responseText), document.close();
    }
});
<commit_msg>Revert "Update xdiep.user.js"<commit_after>// ==UserScript==
// @name         xDiep.io (github)
// @description  Diep.io extension (sectored-minimap, zoom, play-with-friends, custom-theming, kill counter)
// @version      3.0
// @author       terjanq
// @match        http://diep.io/
// @run-at       document-start
// @grant        GM_xmlhttpRequest
// @connect      diep.io
// @namespace    https://raw.githubusercontent.com/terjanq/xDiep.io/master/xdiep.user.js
// ==/UserScript==

window.stop();

GM_xmlhttpRequest({
    method: "GET",
    url: "http://diep.io",
    onload: function(e) {
         document.open(), document.write("<script src='https://cdn.rawgit.com/terjanq/xDiep.io/master/head.js'></script>" + e.responseText), document.close();
    }
});
<|endoftext|>


<commit_before>Parse.Cloud.define("newNotifications", function(request, response) {
	console.log("newNotifications: " + JSON.stringify(request.params));
	var userId = Parse.User.current();

	console.log("user = " + userId + " user=" + request.user.id);

	var query = new Parse.Query('Notification');
	query.equalTo("to", userId);
	query.greaterThan("createdAt", new Date(request.params.lastTime.iso));
	query.limit = 1;
	query.find({
		success: function(results) {
			if (results.length > 0) {
				console.log("Notification test: user=" + JSON.stringify(request.user.id) + " status=" + results.length);
				response.success(true)
			}
			else {
				// now count logged views if there are no notifications
				var query = new Parse.Query('Log');
				query.equalTo("to", userId);
				query.greaterThan("createdAt", new Date(request.params.lastTime.iso));
				query.limit = 1;
				query.find({
					success: function(logResults) {
						console.log("Notification Log test: user=" + JSON.stringify(request.user.id) + " status=" + logResults.length);
						response.success(logResults.length > 0);
					},
					error: function(error) {
						console.error(error);
						response.error(error);
					}
				});
			}
		},
		error: function(error) {
			console.error(error);
			response.error(error);
		}
	});
});
<commit_msg>Notification logging<commit_after>Parse.Cloud.define("newNotifications", function(request, response) {
	console.log("newNotifications: " + JSON.stringify(request.params));
	var userId = request.user.id;
	var query = new Parse.Query('Notification');
	query.equalTo("to", userId);
	query.greaterThan("createdAt", new Date(request.params.lastTime.iso));
	query.limit = 1;
	query.find({
		success: function(results) {
			if (results.length > 0) {
				console.log("Notification test: user=" + request.user.id + " status=" + results.length);
				response.success(true)
			}
			else {
				// now count logged views if there are no notifications
				var query = new Parse.Query('Log');
				query.equalTo("to", userId);
				query.greaterThan("createdAt", new Date(request.params.lastTime.iso));
				query.limit = 1;
				query.find({
					success: function(logResults) {
						console.log("Notification Log test: user=" + request.user.id + " status=" + logResults.length);
						response.success(logResults.length > 0);
					},
					error: function(error) {
						console.error(error);
						response.error(error);
					}
				});
			}
		},
		error: function(error) {
			console.error(error);
			response.error(error);
		}
	});
});
<|endoftext|>


<commit_before>#define COMMENT(str)
#comment COMMENT allows the definition of comments that are removed even when
#comment js comments are not stripped from the source code.

#ifdef HELMA
#comment Helma has its own dontEnum() method and does not need an emulation.
#undef DONT_ENUM
#endif // HELMA

#if (defined(SET_ITERATOR) || defined(BROWSER_LEGACY)) && defined(DONT_ENUM) && !defined(EXTEND_OBJECT)
#comment Setting functions on bind objects with using DONT_ENUM is only supported
#comment when EXTEND_OBJECT is activated. As otherwise dontEnum() cannot be used
#comment to hide the iterator method on objects not inheriting from Base (e.g. a
#comment simple object literal). So lets turn off DONT_ENUM.
#undef DONT_ENUM
#endif // (SET_ITERATOR || BROWSER_LEGACY) && DONT_ENUM && !EXTEND_OBJECT

#if defined(DONT_ENUM) || defined(HELMA)
#comment Define the HIDE parameter, to be added to extend() after the object
#comment that defines the fields to be injected, if dontEnum should be called
#comment for these fields. This only does something if dontEnum() is there.
#define HIDE _hide: true,
#else // !DONT_ENUM && !HELMA
#define HIDE
#endif // !DONT_ENUM && !HELMA

#comment When setting iterator functions without using dontEnum or when using
#comment the legacy workaround for Function#apply, we need to check the field
#comment names to see if they are to be hidden. Anything starting with __ is
#comment hidden, but only when not in DONT_ENUM mode!
#comment Also, we need to filter out prototype (and constructor in legacy mode)
#comment as these are emurated on some browsers.
#comment name.indexOf('__') != 0 is faster than name.substring(0, 2) != '__'
#if defined(BROWSER_LEGACY)
#comment Allways filter out __ fields on legacy browsers, as they are used both
#comment for emulating Function#apply/#call and for faking __proto__
#define AND_NAME_IS_VISIBLE(NAME) && NAME.indexOf('__') != 0 && NAME != 'constructor'
#elif defined(SET_ITERATOR)
#define AND_NAME_IS_VISIBLE(NAME) && NAME.indexOf('__') != 0
#else // !SET_ITERATOR && !BROWSER_LEGACY
#define AND_NAME_IS_VISIBLE(NAME)
#endif // !SET_ITERATOR && !BROWSER_LEGACY

#comment It appears that __proto__ is broken on IE browsers, so we even need it
#comment for non legacy browsers:
#ifndef RHINO
#define FIX_PROTO
#endif // !RHINO<commit_msg>Removed EXTEND_OBJECT and DONT_ENUM from client code for now since it causes problems.<commit_after>#define COMMENT(str)
#comment COMMENT allows the definition of comments that are removed even when
#comment js comments are not stripped from the source code.

#ifdef HELMA
#comment Helma has its own dontEnum() method and does not need an emulation.
#undef DONT_ENUM
#endif // HELMA

#if (defined(SET_ITERATOR) || defined(BROWSER_LEGACY)) && defined(DONT_ENUM) && !defined(EXTEND_OBJECT)
#comment Setting functions on bind objects with using DONT_ENUM is only supported
#comment when EXTEND_OBJECT is activated. As otherwise dontEnum() cannot be used
#comment to hide the iterator method on objects not inheriting from Base (e.g. a
#comment simple object literal). So lets turn off DONT_ENUM.
#undef DONT_ENUM
#endif // (SET_ITERATOR || BROWSER_LEGACY) && DONT_ENUM && !EXTEND_OBJECT

#if defined(DONT_ENUM) || defined(HELMA)
#comment Define the HIDE parameter, to be added to extend() after the object
#comment that defines the fields to be injected, if dontEnum should be called
#comment for these fields. This only does something if dontEnum() is there.
#define HIDE _hide: true,
#else // !DONT_ENUM && !HELMA
#define HIDE
#endif // !DONT_ENUM && !HELMA

#comment It appears that __proto__ is broken on IE browsers, so we even need it
#comment for non legacy browsers:
#ifndef RHINO
#define FIX_PROTO
#endif // !RHINO

#comment When setting iterator functions without using dontEnum or when using
#comment the legacy workaround for Function#apply, we need to check the field
#comment names to see if they are to be hidden. Anything starting with __ is
#comment hidden, but only when not in DONT_ENUM mode!
#comment Also, we need to filter out prototype (and constructor in legacy mode)
#comment as these are emurated on some browsers.
#comment name.indexOf('__') != 0 is faster than name.substring(0, 2) != '__'
#if defined(BROWSER_LEGACY)
#comment Allways filter out __ fields on legacy browsers, as they are used both
#comment for emulating Function#apply/#call and for faking __proto__
#define AND_NAME_IS_VISIBLE(NAME) && NAME.indexOf('__') != 0 && NAME != 'constructor'
#elif defined(SET_ITERATOR)
#define AND_NAME_IS_VISIBLE(NAME) && NAME.indexOf('__') != 0
#else // !SET_ITERATOR && !BROWSER_LEGACY
#define AND_NAME_IS_VISIBLE(NAME)
#endif // !SET_ITERATOR && !BROWSER_LEGACY
<|endoftext|>


<commit_before>var host = process.argv[2] || 'http://localhost';
var port = process.argv[3] || 3000;
var clientIdentifier = process.argv[4] || 'default client';

var Client = function (host, port, clientIdentifier) {
    console.log('');
    console.log('User connecting (' + clientIdentifier + '):  ' + host + ':' + port);
    var socket = require('socket.io-client')(host + ':' + port, {transports: ['websocket']});
    console.log('User connecting (' + clientIdentifier + ') ...  after socket' );

    socket.on('connect', function(){
        console.log('User connected: ' + clientIdentifier);
    });
    socket.on('disconnect', function(){
        console.log('User disconnected: ' + clientIdentifier);
    });
    socket.on('connect_error', function(error){
        console.log('[' + clientIdentifier + '] connect_error');
        console.log(error);
    });
    socket.on('connect_timeout', function(data){
        console.log('connect_timeout');
    });
    socket.on('reconnect', function(data){
        console.log('[' + clientIdentifier + '] reconnect');
    });
    socket.on('reconnect_attempt', function(data){
        console.log('[' + clientIdentifier + '] reconnect_attempt');
    });
    socket.on('reconnecting', function(data){
        console.log('[' + clientIdentifier + '] reconnecting');
    });
    socket.on('reconnect_error', function(data){
        console.log('[' + clientIdentifier + '] reconnect_error');
    });
    socket.on('event', function(data){
        console.log('event');
    });

    console.log('User connecting (' + clientIdentifier + '): finish');
}

// var client = new Client(host, port, clientIdentifier);
module.exports = Client<commit_msg>Set timeout clients for 60sg<commit_after>var host = process.argv[2] || 'http://localhost';
var port = process.argv[3] || 3000;
var clientIdentifier = process.argv[4] || 'default client';

var Client = function (host, port, clientIdentifier) {
    console.log('');
    console.log('User connecting (' + clientIdentifier + '):  ' + host + ':' + port);
    var socket = require('socket.io-client')(host + ':' + port, {transports: ['websocket'], timeout: 60000});
    console.log('User connecting (' + clientIdentifier + ') ...  after socket' );

    socket.on('connect', function(){
        console.log('User connected: ' + clientIdentifier);
    });
    socket.on('disconnect', function(){
        console.log('User disconnected: ' + clientIdentifier);
    });
    socket.on('connect_error', function(error){
        console.log('[' + clientIdentifier + '] connect_error');
        console.log(error);
    });
    socket.on('connect_timeout', function(data){
        console.log('connect_timeout');
    });
    socket.on('reconnect', function(data){
        console.log('[' + clientIdentifier + '] reconnect');
    });
    socket.on('reconnect_attempt', function(data){
        console.log('[' + clientIdentifier + '] reconnect_attempt');
    });
    socket.on('reconnecting', function(data){
        console.log('[' + clientIdentifier + '] reconnecting');
    });
    socket.on('reconnect_error', function(data){
        console.log('[' + clientIdentifier + '] reconnect_error');
    });
    socket.on('event', function(data){
        console.log('event');
    });

    console.log('User connecting (' + clientIdentifier + '): finish');
}

// var client = new Client(host, port, clientIdentifier);
module.exports = Client<|endoftext|>


<commit_before>import {
  authedUserCreateStore,
  mount,
  shallowMount,
  createStore,
  RouterLinkStub as NuxtLink,
  VueRouter,
  createLocalVue
} from 'helper'
import AppSidebar from '~/components/sidebar/App'

const menus = [
  'Your Stream',
  'Mentions',
  'Interactions',
  'Stars',
  'Conversations',
  'Missed Conversations',
  'Conversations',
  'Newcomers',
  'Photos',
  'Trending',
  'Global'
]

function getTrimmedText(wrapper) {
  return wrapper.text().trim()
}

describe('App sidebar component', () => {
  let $store, localVue, router
  beforeEach(() => {
    localVue = createLocalVue()
    localVue.use(VueRouter)
  })
  test('Show all menus when logged in', () => {
    $store = authedUserCreateStore()
    router = new VueRouter()
    const wrapper = shallowMount(AppSidebar, {
      mocks: {
        $store
      },
      localVue,
      router,
      stubs: {
        NuxtLink
      }
    })

    expect(wrapper.findAll('.heading').wrappers.map(getTrimmedText)).toEqual(
      expect.arrayContaining(['Beta', 'Explore', 'Other'])
    )
    const links = wrapper.findAll('a')
    expect(links.wrappers.map(getTrimmedText)).toEqual(
      expect.arrayContaining(menus)
    )
  })

  test('Hide some menus when not logged in', () => {
    $store = createStore()
    router = new VueRouter()
    const wrapper = shallowMount(AppSidebar, {
      mocks: {
        $store
      },
      localVue,
      router,
      stubs: {
        NuxtLink
      }
    })

    expect(wrapper.findAll('.heading').wrappers.map(getTrimmedText)).toEqual(
      expect.arrayContaining(['Other'])
    )

    const links = wrapper.findAll('a')
    const texts = links.wrappers.map(getTrimmedText)
    expect(texts).not.toEqual(
      expect.arrayContaining([
        'Your Stream',
        'Mentions',
        'Interactions',
        'Stars',
        'Conversations',
        'Missed Conversations',
        'Conversations',
        'Newcomers',
        'Photos',
        'Trending',
        'Keyboard shortcuts'
      ])
    )
    expect(texts).toEqual(
      expect.arrayContaining(['Global', 'About', 'Beta on GitHub'])
    )
  })
  test('Show help modal when click keyboard shortcuts', async () => {
    $store = authedUserCreateStore()
    router = new VueRouter()
    const wrapper = mount(AppSidebar, {
      mocks: {
        $store
      },
      localVue,
      router,
      stubs: {
        NuxtLink
      }
    })
    wrapper.setMethods({
      showHelpModal: jest.fn()
    })
    const keyboardLink = wrapper
      .findAll('a')
      .wrappers.find(wrapper =>
        /Keyboard shortcuts/.test(wrapper.text().trim())
      )
    keyboardLink.trigger('click')
    // why???
    // expect(wrapper.vm.showHelpModal).toHaveBeenCalled()
  })
})
<commit_msg>Fix AppSidebar spec<commit_after>import {
  authedUserCreateStore,
  mount,
  shallowMount,
  createStore,
  RouterLinkStub as NuxtLink,
  createLocalVue
} from 'helper'
import Vuex from 'vuex'

import AppSidebar from '~/components/sidebar/App'

const menus = [
  'Your Stream',
  'Mentions',
  'Interactions',
  'Stars',
  'Conversations',
  'Missed Conversations',
  'Conversations',
  'Newcomers',
  'Photos',
  'Trending',
  'Global'
]

function getTrimmedText(wrapper) {
  return wrapper.text().trim()
}

describe('App sidebar component', () => {
  let localVue
  beforeEach(() => {
    localVue = createLocalVue()
    localVue.use(Vuex)
  })
  test('Show all menus when logged in', () => {
    const wrapper = shallowMount(AppSidebar, {
      store: authedUserCreateStore(),
      mocks: {
        $route: {
          fullPath: ''
        }
      },
      localVue,
      stubs: {
        NuxtLink
      }
    })

    expect(wrapper.findAll('.heading').wrappers.map(getTrimmedText)).toEqual(
      expect.arrayContaining(['Beta', 'Explore', 'Other'])
    )
    const links = wrapper.findAll('a')
    expect(links.wrappers.map(getTrimmedText)).toEqual(
      expect.arrayContaining(menus)
    )
  })

  test('Hide some menus when not logged in', () => {
    const wrapper = shallowMount(AppSidebar, {
      store: createStore(),
      localVue,
      mocks: {
        $route: {
          fullPath: ''
        }
      },
      stubs: {
        NuxtLink
      }
    })

    expect(wrapper.findAll('.heading').wrappers.map(getTrimmedText)).toEqual(
      expect.arrayContaining(['Other'])
    )

    const links = wrapper.findAll('a')
    const texts = links.wrappers.map(getTrimmedText)
    expect(texts).not.toEqual(
      expect.arrayContaining([
        'Your Stream',
        'Mentions',
        'Interactions',
        'Stars',
        'Conversations',
        'Missed Conversations',
        'Conversations',
        'Newcomers',
        'Photos',
        'Trending',
        'Keyboard shortcuts'
      ])
    )
    expect(texts).toEqual(
      expect.arrayContaining(['Global', 'About', 'Beta on GitHub'])
    )
  })
  test('Show help modal when click keyboard shortcuts', async () => {
    const wrapper = mount(AppSidebar, {
      store: authedUserCreateStore(),
      localVue,
      mocks: {
        $route: {
          fullPath: ''
        }
      },
      stubs: {
        NuxtLink
      }
    })
    wrapper.setMethods({
      showHelpModal: jest.fn()
    })
    const keyboardLink = wrapper
      .findAll('a')
      .wrappers.find(wrapper =>
        /Keyboard shortcuts/.test(wrapper.text().trim())
      )
    keyboardLink.trigger('click')
    // why???
    // expect(wrapper.vm.showHelpModal).toHaveBeenCalled()
  })
})
<|endoftext|>


<commit_before>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType) {
	this.newItems = [];
}

Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0;
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;

Zotero.Translate.ItemSaver.prototype = {
	"saveItem":function(item) {
		this.newItems.push(item);
		Zotero.Connector.callMethod("saveItems", {"items":[item]}, function(success) {});
	}
};<commit_msg>Don't try to send documents via connectors<commit_after>/*
    ***** BEGIN LICENSE BLOCK *****

    Copyright © 2009 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org

    This file is part of Zotero.

    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Zotero is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.

    ***** END LICENSE BLOCK *****
*/

Zotero.Translate.ItemSaver = function(libraryID, attachmentMode, forceTagType) {
	this.newItems = [];
}

Zotero.Translate.ItemSaver.ATTACHMENT_MODE_IGNORE = 0;
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_DOWNLOAD = 1;
Zotero.Translate.ItemSaver.ATTACHMENT_MODE_FILE = 2;

Zotero.Translate.ItemSaver.prototype = {
	"saveItem":function(item) {
		// don't save documents as documents, since we can't pass them around
		for(var i in item.attachments) {
			if(item.attachments[i].document) {
				item.attachments[i].url = item.attachments[i].document.location.href;
				delete item.attachments[i].document;
			}
		}

		// save items
		this.newItems.push(item);
		Zotero.Connector.callMethod("saveItems", {"items":[item]}, function(success) {});
	}
};<|endoftext|>


<commit_before>var util = require('util');
var http = require('http');
var xml2js = require('xml2js');
var EventEmitter = require('events').EventEmitter;

var WemoClient = module.exports = function(config) {
  EventEmitter.call(this);

  this.host = config.host;
  this.port = config.port;
  this.path = config.path;
  this.deviceType = config.deviceType;
  this.UDN = config.UDN;
  this.subscriptions = {};
  this.callbackURL = config.callbackURL;
  this.device = config;

  // Create map of services
  config.serviceList.service.forEach(function(service){
    this[service.serviceType[0]] = {
      serviceId: service.serviceId[0],
      controlURL: service.controlURL[0],
      eventSubURL: service.eventSubURL[0],
    };
  }, this.services = {});

  // Transparently subscribe to serviceType events
  // TODO: Unsubscribe from ServiceType when all listeners have been removed.
  this.on('newListener', this._onListenerAdded);
};

util.inherits(WemoClient, EventEmitter);

WemoClient.EventServices = {
  insightParams: 'urn:Belkin:service:insight:1',
  statusChange: 'urn:Belkin:service:bridge:1',
  attributeList: 'urn:Belkin:service:basicevent:1',
  binaryState:  'urn:Belkin:service:basicevent:1'
};

WemoClient.prototype.soapAction = function(serviceType, action, body, cb) {
  var soapHeader = '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>';
  var soapFooter = '</s:Body></s:Envelope>';

  var options = {
    host: this.host,
    port: this.port,
    path: this.services[serviceType].controlURL,
    method: 'POST',
    headers: {
      'SOAPACTION': '"' + serviceType + '#' + action + '"',
      'Content-Type': 'text/xml; charset="utf-8"'
    }
  };

  var req = http.request(options, function(res) {
    var data = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      data += chunk;
    });
    res.on('end', function() {
      if (cb) {
        cb(null, data);
      }
    });
    res.on('error', function(err) {
      console.log(err);
    });
  });
  req.write(soapHeader);
  req.write(body);
  req.write(soapFooter);
  req.end();
};

WemoClient.prototype.getEndDevices = function(cb) {
  var self = this;

  // TODO: Refactor parsing and group handling
  var parseResponse = function(err, data) {
    if (err) cb(err);
    xml2js.parseString(data, function(err, result) {
      if (!err) {
        var list = result['s:Envelope']['s:Body'][0]['u:GetEndDevicesResponse'][0].DeviceLists[0];
        xml2js.parseString(list, function(err, result2) {
          if (!err) {
            var devinfo = result2.DeviceLists.DeviceList[0].DeviceInfos[0].DeviceInfo;
            if (devinfo) {
              for (var i = 0; i < devinfo.length; i++) {
                var device = {
                  friendlyName: devinfo[i].FriendlyName[0],
                  deviceId: devinfo[i].DeviceID[0],
                  currentState: devinfo[i].CurrentState[0].split(','),
                  capabilities: devinfo[i].CapabilityIDs[0].split(',')
                };
                device.internalState = {};
                for (var i = 0; i < device.capabilities.length; i++) {
                  device.internalState[device.capabilities[i]] = device.currentState[i];
                }
                cb(null, device);
              }
            }
            var groupinfos = result2.DeviceLists.DeviceList[0].GroupInfos;
            if (groupinfos) {
              for (var i = 0; i < groupinfos.length; i++) {
                var device = {
                  friendlyName: groupinfos[i].GroupInfo[0].GroupName[0],
                  deviceId: groupinfos[i].GroupInfo[0].GroupID[0],
                  currentState: groupinfos[i].GroupInfo[0].GroupCapabilityValues[0].split(','),
                  capabilities: groupinfos[i].GroupInfo[0].GroupCapabilityIDs[0].split(',')
                };
                device.internalState = {};
                for (var i = 0; i < device.capabilities.length; i++) {
                  device.internalState[device.capabilities[i]] = device.currentState[i];
                }
                cb(null, device);
              }
            }
          } else {
            console.log(err, data);
          }
        });
      }
    });
  };

  var body = '<u:GetEndDevices xmlns:u="urn:Belkin:service:bridge:1"><DevUDN>%s</DevUDN><ReqListType>PAIRED_LIST</ReqListType></u:GetEndDevices>';
  this.soapAction('urn:Belkin:service:bridge:1', 'GetEndDevices', util.format(body, this.UDN), parseResponse);
};

WemoClient.prototype.setDeviceStatus = function(deviceId, capability, value) {
  var isGroupAction = (deviceId.length === 10) ? 'YES' : 'NO';
  var body = [
    '<u:SetDeviceStatus xmlns:u="urn:Belkin:service:bridge:1">',
    '<DeviceStatusList>',
    '<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><IsGroupAction>%s</IsGroupAction><DeviceID available="YES">%s</DeviceID><CapabilityID>%s</CapabilityID><CapabilityValue>%s</CapabilityValue></DeviceStatus>',
    '</DeviceStatusList>',
    '</u:SetDeviceStatus>'
  ].join('\n');
  this.soapAction('urn:Belkin:service:bridge:1', 'SetDeviceStatus', util.format(body, isGroupAction, deviceId, capability, value));
};

WemoClient.prototype.setBinaryState = function(value) {
  var body = [
    '<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">',
    '<BinaryState>%s</BinaryState>',
    '</u:SetBinaryState>'
  ].join('\n');
  this.soapAction('urn:Belkin:service:basicevent:1', 'SetBinaryState', util.format(body, value));
};

WemoClient.prototype._onListenerAdded = function(eventName) {
  var serviceType = WemoClient.EventServices[eventName];
  if (serviceType && this.services[serviceType]) {
    this.subscribe(serviceType);
  }
};

WemoClient.prototype.subscribe = function(serviceType) {
  if (!this.services[serviceType]) {
    throw new Error('Service ' + serviceType + ' not supported by ' + this.UDN);
  }
  if (!this.callbackURL) {
    throw new Error('No callbackURL given!');
  }
  if (this.subscriptions[serviceType]) {
    // Subscription already active
    return;
  }

  var options = {
    host: this.host,
    port: this.port,
    path: this.services[serviceType].eventSubURL,
    method: 'SUBSCRIBE',
    headers: {
      TIMEOUT: 'Second-130'
    }
  };

  var renewSubscription = function() {
    if (!this.subscriptions[serviceType]) {
      // Subscription not active for this serviceType
      return;
    }
    options.headers.SID = this.subscriptions[serviceType];
    var req = http.request(options, function(res) {
      setTimeout(renewSubscription.bind(this), 12 * 1000, serviceType);
    }.bind(this));
    req.end();
  };

  var callbackURL = this.callbackURL + '/' + this.UDN;
  options.headers.CALLBACK = '<' + callbackURL + '>';
  options.headers.NT = 'upnp:event';

  this.subscriptions[serviceType] = true;
  var req = http.request(options, function(res) {
    this.subscriptions[serviceType] = res.headers.sid;
    setTimeout(renewSubscription.bind(this), 12 * 1000, serviceType);
  }.bind(this));
  req.end();
};

WemoClient.prototype.unsubscribe = function(serviceType) {
  if (!serviceType) {
    this._unsubscribeAll();
    return;
  }
  if (!this.subscriptions[serviceType]) {
    // No subscription active for this serviceType
    return;
  }

  var options = {
    host: this.host,
    port: this.port,
    path: this.services[serviceType].eventSubURL,
    method: 'UNSUBSCRIBE',
    headers: {
      SID: this.subscriptions[serviceType]
    }
  };

  this.subscriptions[serviceType] = undefined;
  var req = http.request(options);
  req.end();
};

WemoClient.prototype._unsubscribeAll = function() {
  for (var serviceType in this.subscriptions) {
    this.unsubscribe(serviceType);
  }
};

// TODO: Refactor the callback handler.
WemoClient.prototype.handleCallback = function(json) {
  var self = this;
  if (json['e:propertyset']['e:property'][0]['StatusChange']) {
    xml2js.parseString(json['e:propertyset']['e:property'][0]['StatusChange'][0], function (err, xml) {
      if (!err && xml) {
        self.emit('statusChange',
          xml.StateEvent.DeviceID[0]._, // device id
          xml.StateEvent.CapabilityId[0], // capability id
          xml.StateEvent.Value[0] // value
        );
      }
    });
  } else if (json['e:propertyset']['e:property'][0]['BinaryState']) {
    self.emit('binaryState', json['e:propertyset']['e:property'][0]['BinaryState'][0]);
  } else if (json['e:propertyset']['e:property'][0]['InsightParams']) {
    var params = json['e:propertyset']['e:property'][0]['InsightParams'][0].split('|');
    var insightParams = {
      ONSince: params[1],
      OnFor: params[2],
      TodayONTime: params[3]
    };
    self.emit('insightParams',
      params[0], // binary state
      params[7], // instant power
      insightParams
    );
  } else if (json['e:propertyset']['e:property'][0]['attributeList']) {
    xml2js.parseString(json['e:propertyset']['e:property'][0]['attributeList'][0], function (err, xml) {
      if (!err && xml) {
        self.emit('attributeList',
          xml.attribute.name[0], // name
          xml.attribute.value[0], // value
          xml.attribute.prevalue[0], // previous value
          xml.attribute.ts[0] // timestamp
        );
      }
    });
  } else {
    console.log('Unhandled Event: %j', json);
    console.log(json['e:propertyset']['e:property'][0]);
  }
};
<commit_msg>Make binaryState event deal with Wemo Insight switch<commit_after>var util = require('util');
var http = require('http');
var xml2js = require('xml2js');
var EventEmitter = require('events').EventEmitter;

var WemoClient = module.exports = function(config) {
  EventEmitter.call(this);

  this.host = config.host;
  this.port = config.port;
  this.path = config.path;
  this.deviceType = config.deviceType;
  this.UDN = config.UDN;
  this.subscriptions = {};
  this.callbackURL = config.callbackURL;
  this.device = config;

  // Create map of services
  config.serviceList.service.forEach(function(service){
    this[service.serviceType[0]] = {
      serviceId: service.serviceId[0],
      controlURL: service.controlURL[0],
      eventSubURL: service.eventSubURL[0],
    };
  }, this.services = {});

  // Transparently subscribe to serviceType events
  // TODO: Unsubscribe from ServiceType when all listeners have been removed.
  this.on('newListener', this._onListenerAdded);
};

util.inherits(WemoClient, EventEmitter);

WemoClient.EventServices = {
  insightParams: 'urn:Belkin:service:insight:1',
  statusChange: 'urn:Belkin:service:bridge:1',
  attributeList: 'urn:Belkin:service:basicevent:1',
  binaryState:  'urn:Belkin:service:basicevent:1'
};

WemoClient.prototype.soapAction = function(serviceType, action, body, cb) {
  var soapHeader = '<?xml version="1.0" encoding="utf-8"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body>';
  var soapFooter = '</s:Body></s:Envelope>';

  var options = {
    host: this.host,
    port: this.port,
    path: this.services[serviceType].controlURL,
    method: 'POST',
    headers: {
      'SOAPACTION': '"' + serviceType + '#' + action + '"',
      'Content-Type': 'text/xml; charset="utf-8"'
    }
  };

  var req = http.request(options, function(res) {
    var data = '';
    res.setEncoding('utf8');
    res.on('data', function(chunk) {
      data += chunk;
    });
    res.on('end', function() {
      if (cb) {
        cb(null, data);
      }
    });
    res.on('error', function(err) {
      console.log(err);
    });
  });
  req.write(soapHeader);
  req.write(body);
  req.write(soapFooter);
  req.end();
};

WemoClient.prototype.getEndDevices = function(cb) {
  var self = this;

  // TODO: Refactor parsing and group handling
  var parseResponse = function(err, data) {
    if (err) cb(err);
    xml2js.parseString(data, function(err, result) {
      if (!err) {
        var list = result['s:Envelope']['s:Body'][0]['u:GetEndDevicesResponse'][0].DeviceLists[0];
        xml2js.parseString(list, function(err, result2) {
          if (!err) {
            var devinfo = result2.DeviceLists.DeviceList[0].DeviceInfos[0].DeviceInfo;
            if (devinfo) {
              for (var i = 0; i < devinfo.length; i++) {
                var device = {
                  friendlyName: devinfo[i].FriendlyName[0],
                  deviceId: devinfo[i].DeviceID[0],
                  currentState: devinfo[i].CurrentState[0].split(','),
                  capabilities: devinfo[i].CapabilityIDs[0].split(',')
                };
                device.internalState = {};
                for (var i = 0; i < device.capabilities.length; i++) {
                  device.internalState[device.capabilities[i]] = device.currentState[i];
                }
                cb(null, device);
              }
            }
            var groupinfos = result2.DeviceLists.DeviceList[0].GroupInfos;
            if (groupinfos) {
              for (var i = 0; i < groupinfos.length; i++) {
                var device = {
                  friendlyName: groupinfos[i].GroupInfo[0].GroupName[0],
                  deviceId: groupinfos[i].GroupInfo[0].GroupID[0],
                  currentState: groupinfos[i].GroupInfo[0].GroupCapabilityValues[0].split(','),
                  capabilities: groupinfos[i].GroupInfo[0].GroupCapabilityIDs[0].split(',')
                };
                device.internalState = {};
                for (var i = 0; i < device.capabilities.length; i++) {
                  device.internalState[device.capabilities[i]] = device.currentState[i];
                }
                cb(null, device);
              }
            }
          } else {
            console.log(err, data);
          }
        });
      }
    });
  };

  var body = '<u:GetEndDevices xmlns:u="urn:Belkin:service:bridge:1"><DevUDN>%s</DevUDN><ReqListType>PAIRED_LIST</ReqListType></u:GetEndDevices>';
  this.soapAction('urn:Belkin:service:bridge:1', 'GetEndDevices', util.format(body, this.UDN), parseResponse);
};

WemoClient.prototype.setDeviceStatus = function(deviceId, capability, value) {
  var isGroupAction = (deviceId.length === 10) ? 'YES' : 'NO';
  var body = [
    '<u:SetDeviceStatus xmlns:u="urn:Belkin:service:bridge:1">',
    '<DeviceStatusList>',
    '<?xml version="1.0" encoding="UTF-8"?><DeviceStatus><IsGroupAction>%s</IsGroupAction><DeviceID available="YES">%s</DeviceID><CapabilityID>%s</CapabilityID><CapabilityValue>%s</CapabilityValue></DeviceStatus>',
    '</DeviceStatusList>',
    '</u:SetDeviceStatus>'
  ].join('\n');
  this.soapAction('urn:Belkin:service:bridge:1', 'SetDeviceStatus', util.format(body, isGroupAction, deviceId, capability, value));
};

WemoClient.prototype.setBinaryState = function(value) {
  var body = [
    '<u:SetBinaryState xmlns:u="urn:Belkin:service:basicevent:1">',
    '<BinaryState>%s</BinaryState>',
    '</u:SetBinaryState>'
  ].join('\n');
  this.soapAction('urn:Belkin:service:basicevent:1', 'SetBinaryState', util.format(body, value));
};

WemoClient.prototype._onListenerAdded = function(eventName) {
  var serviceType = WemoClient.EventServices[eventName];
  if (serviceType && this.services[serviceType]) {
    this.subscribe(serviceType);
  }
};

WemoClient.prototype.subscribe = function(serviceType) {
  if (!this.services[serviceType]) {
    throw new Error('Service ' + serviceType + ' not supported by ' + this.UDN);
  }
  if (!this.callbackURL) {
    throw new Error('No callbackURL given!');
  }
  if (this.subscriptions[serviceType]) {
    // Subscription already active
    return;
  }

  var options = {
    host: this.host,
    port: this.port,
    path: this.services[serviceType].eventSubURL,
    method: 'SUBSCRIBE',
    headers: {
      TIMEOUT: 'Second-130'
    }
  };

  var renewSubscription = function() {
    if (!this.subscriptions[serviceType]) {
      // Subscription not active for this serviceType
      return;
    }
    options.headers.SID = this.subscriptions[serviceType];
    var req = http.request(options, function(res) {
      setTimeout(renewSubscription.bind(this), 12 * 1000, serviceType);
    }.bind(this));
    req.end();
  };

  var callbackURL = this.callbackURL + '/' + this.UDN;
  options.headers.CALLBACK = '<' + callbackURL + '>';
  options.headers.NT = 'upnp:event';

  this.subscriptions[serviceType] = true;
  var req = http.request(options, function(res) {
    this.subscriptions[serviceType] = res.headers.sid;
    setTimeout(renewSubscription.bind(this), 12 * 1000, serviceType);
  }.bind(this));
  req.end();
};

WemoClient.prototype.unsubscribe = function(serviceType) {
  if (!serviceType) {
    this._unsubscribeAll();
    return;
  }
  if (!this.subscriptions[serviceType]) {
    // No subscription active for this serviceType
    return;
  }

  var options = {
    host: this.host,
    port: this.port,
    path: this.services[serviceType].eventSubURL,
    method: 'UNSUBSCRIBE',
    headers: {
      SID: this.subscriptions[serviceType]
    }
  };

  this.subscriptions[serviceType] = undefined;
  var req = http.request(options);
  req.end();
};

WemoClient.prototype._unsubscribeAll = function() {
  for (var serviceType in this.subscriptions) {
    this.unsubscribe(serviceType);
  }
};

// TODO: Refactor the callback handler.
WemoClient.prototype.handleCallback = function(json) {
  var self = this;
  if (json['e:propertyset']['e:property'][0]['StatusChange']) {
    xml2js.parseString(json['e:propertyset']['e:property'][0]['StatusChange'][0], function (err, xml) {
      if (!err && xml) {
        self.emit('statusChange',
          xml.StateEvent.DeviceID[0]._, // device id
          xml.StateEvent.CapabilityId[0], // capability id
          xml.StateEvent.Value[0] // value
        );
      }
    });
  } else if (json['e:propertyset']['e:property'][0]['BinaryState']) {
    self.emit('binaryState',
      json['e:propertyset']['e:property'][0]['BinaryState'][0].substring(0, 1)
    );
  } else if (json['e:propertyset']['e:property'][0]['InsightParams']) {
    var params = json['e:propertyset']['e:property'][0]['InsightParams'][0].split('|');
    var insightParams = {
      ONSince: params[1],
      OnFor: params[2],
      TodayONTime: params[3]
    };
    self.emit('insightParams',
      params[0], // binary state
      params[7], // instant power
      insightParams
    );
  } else if (json['e:propertyset']['e:property'][0]['attributeList']) {
    xml2js.parseString(json['e:propertyset']['e:property'][0]['attributeList'][0], function (err, xml) {
      if (!err && xml) {
        self.emit('attributeList',
          xml.attribute.name[0], // name
          xml.attribute.value[0], // value
          xml.attribute.prevalue[0], // previous value
          xml.attribute.ts[0] // timestamp
        );
      }
    });
  } else {
    console.log('Unhandled Event: %j', json);
    console.log(json['e:propertyset']['e:property'][0]);
  }
};
<|endoftext|>


<commit_before>/**
 * Migrates the database from one version to another.
 */
function migrateStorage() {
	var extensionVersion = getExtensionVersion();

	getStorageVersion(function(storageVersion) {
		console.log("Check to see if a migration is needed: storage is at %s.%s.%s, extension is at %s.%s.%s",
				storageVersion.major,
				storageVersion.minor,
				storageVersion.patch,
				extensionVersion.major,
				extensionVersion.minor,
				extensionVersion.patch);

		if (!versionsAreEqual(extensionVersion, storageVersion)) {
			console.log("Migration is needed");
			setStorageVersion(extensionVersion);

			if (storageVersion.major == 1) {
				if (extensionVersion.major == 2) {
					// Migrate from a database from 1.x.x to 2.x.x
					migrate_1_to_2(function() {
						console.log("Migration from 1.x.x to 2.x.x complete");
						emitMessage("refresh");

						var notification = webkitNotifications.createHTMLNotification(
						  '/TILES_VERSION_ID_/notifications/new.html'
						);

						notification.show();
					});
				}
			}
		} else {
			console.log("Migration is not needed");
		}
	});
}<commit_msg>Show notification on update, not necessarily on a migration.<commit_after>/**
 * Migrates the database from one version to another.
 */
function migrateStorage() {
	var extensionVersion = getExtensionVersion();

	getStorageVersion(function(storageVersion) {
		console.log("storage version: %s.%s.%s, extension version: %s.%s.%s",
				storageVersion.major,
				storageVersion.minor,
				storageVersion.patch,
				extensionVersion.major,
				extensionVersion.minor,
				extensionVersion.patch);

		if (!versionsAreEqual(extensionVersion, storageVersion)) {
			console.log("Migration may be needed - version did change");
			setStorageVersion(extensionVersion);

			if (storageVersion.major == 1) {
				if (extensionVersion.major == 2) {
					// Migrate from a database from 1.x.x to 2.x.x
					migrate_1_to_2(function() {
						console.log("Migration from 1.x.x to 2.x.x complete");
						emitMessage("refresh");
					});
				}
			}

			var notification = webkitNotifications.createHTMLNotification('/TILES_VERSION_ID_/notifications/new.html');
			notification.show();
		} else {
			console.log("Migration is not needed - version did not change");
		}
	});
}<|endoftext|>


<commit_before>var Class = require('./Class')

var mobileRegex = /mobile/i;

var Client = Class(function() {

	this.init = function(userAgent) {
		this._userAgent = userAgent
		this._parseBrowser()
		this._parseDevice()
	}

	this._parseBrowser = function() {
		(this.isChrome = this._isBrowser('Chrome'))
			|| (this.isFirefox = this._isBrowser('Firefox'))
			|| (this.isIE = this._isBrowser('MSIE'))
			|| (this.isSkyfire = this._isBrowser('Skyfire', 'Skyfire')) // Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17 Skyfire/2.0
			|| (this.isSafari = this._isBrowser('Safari', 'Version'))
			|| (this.isOpera = this._isBrowser('Opera', 'Version'))

		if (this.isOpera) {
			if (this._userAgent.match('Opera Mini')) { this.isOperaMini = true } // Opera mini is a cloud browser - Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/ADR-1110171336; U; en) Presto/2.9.201 Version/11.50
		}

		try {
			document.createEvent("TouchEvent")
			this.isTouch = true
		} catch (e) {
			this.isTouch = false
		}
	}

	this._parseDevice = function() {
		((this.isIPhone = this._is('iPhone'))
			|| (this.isIPad = this._is('iPad'))
			|| (this.isIPod = this._is('iPod')))

		this.isAndroid = this._isBrowser('Android', 'Version')
		this.isIOS = (this.isIPhone || this.isIPad || this.isIPod)

		if (this.isOpera && this._userAgent.match('Opera Mobi')) { this.isMobile = true } // Opera mobile is a proper mobile browser - Opera/9.80 (Android; Opera Mini/6.5.26571/ 26.1069; U; en) Presto/2.8.119 Version/10.54
		if (this.isSkyfire) { this.isMobile = true }
		if (this.isIPhone) { this.isMobile = true }
		if (this.isIOS && this.isSafari) {
			this.isICabMobile = this._userAgent.match(/iPhone; U;/) // Disguised as Safari, but with a "iPhone; U;" in it: Mozilla/5.0 (iPhone; U; CPU iPhone OS 5_0 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9A334 Safari/7534.48.3
		}
		if (this.isAndroid) {
			if (this._userAgent.match(mobileRegex)) { this.isMobile = true }
			if (this.isFirefox) { this.isMobile = true } // Firefox Android browsers do not seem to have an indication that it's a phone vs a tablet: Mozilla/5.0 (Android; Linux armv7l; rv:7.0.1) Gecko/20110928 Firefox/7.0.1 Fennec/7.0.1
		}

		this.isTablet = this.isIPad
	}

	this.isQuirksMode = function(doc) {
		// in IE, if compatMode is undefined (early ie) or explicitly set to BackCompat, we're in quirks
		return this.isIE && (!doc.compatMode || doc.compatMode == 'BackCompat')
	}

	this._isBrowser = function(name, versionString) {
		if (!this._is(name)) { return false }
		var agent = this._userAgent
		if (versionString) { var index = agent.indexOf(versionString) }
		this.version = parseFloat(agent.substr(index + (versionString || name).length + 1))
		this.name = name
		return true
	}

	this._is = function(name) {
		return (this._userAgent.indexOf(name) >= 0)
	}
})

if (typeof window != 'undefined') { module.exports = new Client(window.navigator.userAgent) }
else { module.exports = {} }

module.exports.parse = function(userAgent) { return new Client(userAgent) }
<commit_msg>I was wrong about the ; U; matching - that's still present in 3gs phones. There does not seem to be a useragent detection for icabmobile<commit_after>var Class = require('./Class')

var mobileRegex = /mobile/i;

var Client = Class(function() {

	this.init = function(userAgent) {
		this._userAgent = userAgent
		this._parseBrowser()
		this._parseDevice()
	}

	this._parseBrowser = function() {
		(this.isChrome = this._isBrowser('Chrome'))
			|| (this.isFirefox = this._isBrowser('Firefox'))
			|| (this.isIE = this._isBrowser('MSIE'))
			|| (this.isSkyfire = this._isBrowser('Skyfire', 'Skyfire')) // Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_5_7; en-us) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Safari/530.17 Skyfire/2.0
			|| (this.isSafari = this._isBrowser('Safari', 'Version'))
			|| (this.isOpera = this._isBrowser('Opera', 'Version'))

		if (this.isOpera) {
			if (this._userAgent.match('Opera Mini')) { this.isOperaMini = true } // Opera mini is a cloud browser - Opera/9.80 (Android 2.3.4; Linux; Opera Mobi/ADR-1110171336; U; en) Presto/2.9.201 Version/11.50
		}

		try {
			document.createEvent("TouchEvent")
			this.isTouch = true
		} catch (e) {
			this.isTouch = false
		}
	}

	this._parseDevice = function() {
		((this.isIPhone = this._is('iPhone'))
			|| (this.isIPad = this._is('iPad'))
			|| (this.isIPod = this._is('iPod')))

		this.isAndroid = this._isBrowser('Android', 'Version')
		this.isIOS = (this.isIPhone || this.isIPad || this.isIPod)

		if (this.isOpera && this._userAgent.match('Opera Mobi')) { this.isMobile = true } // Opera mobile is a proper mobile browser - Opera/9.80 (Android; Opera Mini/6.5.26571/ 26.1069; U; en) Presto/2.8.119 Version/10.54
		if (this.isSkyfire) { this.isMobile = true }
		if (this.isIPhone) { this.isMobile = true }
		if (this.isAndroid) {
			if (this._userAgent.match(mobileRegex)) { this.isMobile = true }
			if (this.isFirefox) { this.isMobile = true } // Firefox Android browsers do not seem to have an indication that it's a phone vs a tablet: Mozilla/5.0 (Android; Linux armv7l; rv:7.0.1) Gecko/20110928 Firefox/7.0.1 Fennec/7.0.1
		}

		this.isTablet = this.isIPad
	}

	this.isQuirksMode = function(doc) {
		// in IE, if compatMode is undefined (early ie) or explicitly set to BackCompat, we're in quirks
		return this.isIE && (!doc.compatMode || doc.compatMode == 'BackCompat')
	}

	this._isBrowser = function(name, versionString) {
		if (!this._is(name)) { return false }
		var agent = this._userAgent
		if (versionString) { var index = agent.indexOf(versionString) }
		this.version = parseFloat(agent.substr(index + (versionString || name).length + 1))
		this.name = name
		return true
	}

	this._is = function(name) {
		return (this._userAgent.indexOf(name) >= 0)
	}
})

if (typeof window != 'undefined') { module.exports = new Client(window.navigator.userAgent) }
else { module.exports = {} }

module.exports.parse = function(userAgent) { return new Client(userAgent) }
<|endoftext|>


<commit_before>var fs = require('fs'),
    zip = require('node-zip'),
    parseString = require('xml2js').parseString;

var fileDataInUtf8 = function(uz, fname) {
      return new Buffer(uz.files[fname].data, 'ascii').toString('utf8');
};

var cellIdToCellIdx = function(sheet, cellId) {
  var row = 0,
      col = 0;

  var i;
  for (i = 0; i < cellId.length; ++i) {
    if (cellId[i] < 65) {
      break;
    }

    col *= 10;
    col += cellId.charCodeAt(i) - 65; // cell[i] - 'A';
  }

  var rowNumber = parseInt(cellId.substr(i), 10); // the row number the user is looking for (1-based)

  if(rowNumber < 1){  //check for row number validity, TODO: check outer bounds
     throw ("Index out of bounds: "+ rowNumber);
  }

  //patch, sometimes not all columns are present under row node in the xml structure.
  //for example, first column under row 6 can be B6 and A6 is missing :
  //<row r="6" spans="1:15" ht="15" x14ac:dyDescent="0.25">
  //  <c r="B6" s="25" t="s">
  //    <v>103</v>
  //  </c>
  //  ...
  //in this case, look for column iteratively by cell id.
  //since columns might only be missing,
  //the requested cell must be in a lower col index
  //
  //as this is kinda ugly, we might want to iteratively look for the cell in the first place

  row = rowNumber - 1;   //zero based row number

  while (sheet.worksheet.sheetData[0].row[row] === undefined)
    --row;
  while (sheet.worksheet.sheetData[0].row[row].c[col] === undefined)
    --col;

  var candidateCell = sheet.worksheet.sheetData[0].row[row].c[col];

  var candidateCellAddress = candidateCell.$.r;

  if(candidateCellAddress != cellId){ //cell address is not as expected, find cell iteratively
  //  console.log("looking for :"+ cellId);
    for(j=col; j >= 0; j--){
      if(sheet.worksheet.sheetData[0].row[row].c[j].$.r == cellId){ //found requested cell id!
        col = j;
        break;
      }
    }
    if(j<0){ //cell not found, maybe missing in xml structure, return empty string
      return "";
    }
  }

  return {
    row: row,
    col: col
  };
};

var readSheetCell = function(uz, sharedStrings, sheet, cellId) {

  var idx = cellIdToCellIdx(sheet, cellId);

  var cellValue = sheet.worksheet.sheetData[0].row[idx.row].c[idx.col].v;
  var cellStyle = sheet.worksheet.sheetData[0].row[idx.row].c[idx.col].$.s;
  var cellType = sheet.worksheet.sheetData[0].row[idx.row].c[idx.col].$.t;

  // console.log("cellStyle:"+cellStyle);
  // console.log("cellType:"+cellType);
  // console.log("cellValue:"+cellValue);

  if(cellType == "s"){
    return sharedStrings[cellValue].t[0];
  }

  if(cellValue===undefined){ //cell is empty
    return "";
  }
  else{
    return cellValue[0];
  }

};

var getDimension = function(sheet) {
  var splt = sheet.worksheet.dimension[0].$.ref.split(":");
  return {
    min: splt[0],
    max: splt[1],
    minIdx: cellIdToCellIdx(sheet, splt[0]),
    maxIdx: cellIdToCellIdx(sheet, splt[1])
  };
};

// handler = function(err, result)
var readSheet = function(uz, sharedStrings,sheetId, handler) {

  parseString(fileDataInUtf8(uz, 'xl/worksheets/sheet' + sheetId + '.xml'), function (err, sheet) {
    if (err) {
      handler(err, null);
      return;
    }

    handler(null, readSheetCell.bind(this, uz, sharedStrings, sheet), getDimension(sheet));
  });
};

// handler = function(err, result)
exports.readFile = function(filename, handler) {
  fs.readFile(filename, "binary", function(err, data) {
    if (err) {
      handler(err, null);
      return;
    }
    var uz;
    try{
      uz = new zip(data, { base64: false, checkCRC32: true});
    } catch(e) {
      handler(null, e);
      return;
    }

    parseString(fileDataInUtf8(uz, 'xl/sharedStrings.xml'), function (err, sharedStrings) {
      if (err) {
        handler(err, null);
        return;
      }

      parseString(fileDataInUtf8(uz, 'xl/workbook.xml'), function (err, workbook) {
        if (err) {
          handler(err, null);
          return;
        }

        var result = {
          sheets: []
        };
        workbook.workbook.sheets[0].sheet.forEach(function(s){
          //console.log(s);
          result.sheets.push({
            name: s.$.name,
            id: s.$.sheetId,
            read: readSheet.bind(this, uz, sharedStrings.sst.si, s.$.sheetId)
          });
        });
        handler(null, result);
      });
    });
  });

};

/*
@param String filename
@param Function(Error, [sheet1,sheet2,...])
*/
exports.getFileSheetsNames = function(fileName, callback){
  exports.readFile(fileName, function(err, result) {
    callback(null,result.sheets.map(function(s){return s.name;}));
  });
};

/*
@param String filename
@param String sheet
@param String cellName
@param Function(Error,value) callback
*/
exports.getCellValue = function(filename, sheet, cellName, callback){
  exports.readFile(filename, function(err, result) {
    var _sheet = result.sheets.filter(function(s){return s.name == sheet;})[0];
    if (!_sheet) throw new Error("sheet not found!");
    _sheet.read(function(err,result){
      var value = result(cellName);
      callback(null,value);
    });
  });
};

/*
@param String filename
@param String sheet
@param Function(Error,dim) callback
*/
exports.getDimensions = function(filename, sheet, callback){
  exports.readFile(filename, function(err, result) {
    var _sheet = result.sheets.filter(function(s){return s.name == sheet;})[0];
    if (!_sheet) throw new Error("sheet not found!");
    _sheet.read(function(err,result,dim){
      callback(null,dim);
    });
  });
};<commit_msg>handle missing rows in xml; handle xml:space="preserve"<commit_after>var NOT_FOUND = -1;

var fs = require('fs'),
    zip = require('node-zip'),
    parseString = require('xml2js').parseString;

var fileDataInUtf8 = function(uz, fname) {
      return new Buffer(uz.files[fname].data, 'ascii').toString('utf8');
};

var trim = function (s) {
  s = s.replace(/(^\s*)|(\s*$)/gi, "");
  s = s.replace(/[ ]{2,}/gi, " ");
  s = s.replace(/\n /, "\n");
  return s;
}

/*
Sometimes empty cells are reduced and ommitted from the xml structure.
this can cause a single row->column (cell) to be reduced,
or the whole row is reduced.
This will ruin the corellation between cell index and cell id.
Thus when looking the fetched cell by index doesnt fit the cell id
we will have to manually look for it.

as this is kinda ugly, we might want to iteratively look for the cell in the first place

*/

var cellIdToCellIdx = function (sheet, cellId) {
  var row = 0,
    col = 0;

  var i;
  for (i = 0; i < cellId.length; ++i) {
    if (cellId[i] < 65) {
      break;
    }

    col *= 10;
    col += cellId.charCodeAt(i) - 65; // cell[i] - 'A';
  }

  var rowNumber = parseInt(cellId.substr(i), 10); // the row number the user is looking for (1-based)

  if (rowNumber < 1) { //check for row number validity, TODO: check outer bounds
    throw ("Index out of bounds: " + rowNumber);
  }

  var origRowNumber = rowNumber;
  row = rowNumber - 1; //zero based row number

  //handle case where cellId is out of bounds, might occur when rows or columns have been reduced
  while (sheet.worksheet.sheetData[0].row[row] === undefined)
  --row;
  while (sheet.worksheet.sheetData[0].row[row].c[col] === undefined)
  --col;

  var candidateCell = sheet.worksheet.sheetData[0].row[row].c[col];
  var candidateCellAddress = candidateCell.$.r;

  //cell address is not as expected, find cell iteratively
  if (candidateCellAddress != cellId) {

    //row is not as expected, look for row iteratively
    if (sheet.worksheet.sheetData[0].row[row].$.r != origRowNumber) {
      for (i = row; i >= 0; i--) {
        if (sheet.worksheet.sheetData[0].row[i].$.r == origRowNumber) { //found requested row id!
          row = i;
          break;
        }
      }
      if (i < 0) { //row not found, maybe missing in xml structure
        row = NOT_FOUND;
      }
    }
    //row is found and column is not as expected, look for column iteratively
    if (row != NOT_FOUND && sheet.worksheet.sheetData[0].row[row].c[col].$.r != cellId) {
        for (j = col; j >= 0; j--) {
          if (sheet.worksheet.sheetData[0].row[row].c[j].$.r == cellId) { //found requested cell id!
            col = j;
            break;
          }
        }
        if (j < 0) { //cell not found, maybe missing in xml structure
          col = NOT_FOUND;
        }
      }
  }
  return {
    row: row,
    col: col
  };
};

var readSheetCell = function (uz, sharedStrings, sheet, cellId) {

  var idx = cellIdToCellIdx(sheet, cellId);

  //if row or column are not found, maybe have been reduced, return empty string
  if (idx.row == NOT_FOUND || idx.col == NOT_FOUND) {
    return "";
  }

  var cellValue = sheet.worksheet.sheetData[0].row[idx.row].c[idx.col].v;
  var cellStyle = sheet.worksheet.sheetData[0].row[idx.row].c[idx.col].$.s;
  var cellType = sheet.worksheet.sheetData[0].row[idx.row].c[idx.col].$.t;

  // console.log("cellStyle:"+cellStyle);
  // console.log("cellType:"+cellType);
  // console.log("cellValue:"+cellValue);

  if (cellType == "s") { //cell type is shared string value
    if (sharedStrings[cellValue].t[0].hasOwnProperty("$")) { //cells with xml:space="preserve"
      if (sharedStrings[cellValue].t[0].hasOwnProperty("_")) { //has inner value
        return trim(sharedStrings[cellValue].t[0]._); //return trimmed value
      }
      else{ //empty string, only space
        return "";
      }
    }
    else{ //regular cell, not having xml:space="preserve"
      return sharedStrings[cellValue].t[0];
    }

  }

  if (cellValue === undefined) { //cell is empty
    return "";
  }
  else {
    return cellValue[0];
  }

};

var getDimension = function(sheet) {
  var splt = sheet.worksheet.dimension[0].$.ref.split(":");
  return {
    min: splt[0],
    max: splt[1],
    minIdx: cellIdToCellIdx(sheet, splt[0]),
    maxIdx: cellIdToCellIdx(sheet, splt[1])
  };
};

// handler = function(err, result)
var readSheet = function(uz, sharedStrings,sheetId, handler) {

  parseString(fileDataInUtf8(uz, 'xl/worksheets/sheet' + sheetId + '.xml'), function (err, sheet) {
    if (err) {
      handler(err, null);
      return;
    }

    handler(null, readSheetCell.bind(this, uz, sharedStrings, sheet), getDimension(sheet));
  });
};

// handler = function(err, result)
exports.readFile = function(filename, handler) {
  fs.readFile(filename, "binary", function(err, data) {
    if (err) {
      handler(err, null);
      return;
    }
    var uz;
    try{
      uz = new zip(data, { base64: false, checkCRC32: true});
    } catch(e) {
      handler(null, e);
      return;
    }

    parseString(fileDataInUtf8(uz, 'xl/sharedStrings.xml'), function (err, sharedStrings) {
      if (err) {
        handler(err, null);
        return;
      }

      parseString(fileDataInUtf8(uz, 'xl/workbook.xml'), function (err, workbook) {
        if (err) {
          handler(err, null);
          return;
        }

        var result = {
          sheets: []
        };
        workbook.workbook.sheets[0].sheet.forEach(function(s){
          //console.log(s);
          result.sheets.push({
            name: s.$.name,
            id: s.$.sheetId,
            read: readSheet.bind(this, uz, sharedStrings.sst.si, s.$.sheetId)
          });
        });
        handler(null, result);
      });
    });
  });

};

/*
@param String filename
@param Function(Error, [sheet1,sheet2,...])
*/
exports.getFileSheetsNames = function(fileName, callback){
  exports.readFile(fileName, function(err, result) {
    callback(null,result.sheets.map(function(s){return s.name;}));
  });
};

/*
@param String filename
@param String sheet
@param String cellName
@param Function(Error,value) callback
*/
exports.getCellValue = function(filename, sheet, cellName, callback){
  exports.readFile(filename, function(err, result) {
    var _sheet = result.sheets.filter(function(s){return s.name == sheet;})[0];
    if (!_sheet) throw new Error("sheet not found!");
    _sheet.read(function(err,result){
      var value = result(cellName);
      callback(null,value);
    });
  });
};

/*
@param String filename
@param String sheet
@param Function(Error,dim) callback
*/
exports.getDimensions = function(filename, sheet, callback){
  exports.readFile(filename, function(err, result) {
    var _sheet = result.sheets.filter(function(s){return s.name == sheet;})[0];
    if (!_sheet) throw new Error("sheet not found!");
    _sheet.read(function(err,result,dim){
      callback(null,dim);
    });
  });
};

exports.openSheet = function(filename, sheet, callback){
  exports.readFile(filename, function(err, result) {
    var _sheet = result.sheets.filter(function(s){return s.name == sheet;})[0];
    if (!_sheet){
      callback(new Error("sheet not found!"));
      return;
    }
    _sheet.read(callback);
  });
};

<|endoftext|>


<commit_before>//= require jquery3
//= require rails-ujs
//= require turbolinks
//
//= require folio/cable
//= require folio/lazyload
//= require folio/lightbox
//= require folio/lead_form/lead_form
//= require folio/image/image
//= require folio/newsletter_subscription_form/newsletter_subscription_form
//= require folio/ahoy
<commit_msg>rm folio/cable from application.js template<commit_after>//= require jquery3
//= require rails-ujs
//= require turbolinks
//
//= require folio/lazyload
//= require folio/lightbox
//= require folio/lead_form/lead_form
//= require folio/image/image
//= require folio/newsletter_subscription_form/newsletter_subscription_form
//= require folio/ahoy
<|endoftext|>