<commit_before>'use strict';

var util = require('util');
var pathLib = require('path');
var BBPromise = require('bluebird');
var _ = require('underscore');
var express = require('express');

var abaculus = BBPromise.promisify(require('abaculus'));
var makizushi = BBPromise.promisify(require('makizushi'));

var core, Err;
var app, sources;
var metrics;

var infoHeaders = {};

function reportError(errReporterFunc, err) {
    try {
        errReporterFunc(err);
    } catch (e2) {
        console.error('Unable to report: ' + core.errToStr(err) + '\n\nDue to: ' + core.errToStr(e2));
    }
}

function reportRequestError(err) {
    reportError(function (err) {
        res
            .status(400)
            .header('Cache-Control', 'public, s-maxage=30, max-age=30')
            .json(err.message || 'error/unknown');
        core.log('error', err);
        metrics.increment(err.metrics || 'err.unknown');
    }, err);
}

function filterJson(query, data) {
    if ('summary' in query) {
        data = _(data).reduce(function (memo, layer) {
            memo[layer.name] = {
                features: layer.features.length,
                jsonsize: JSON.stringify(layer).length
            };
            return memo;
        }, {});
    } else if ('nogeo' in query) {
        // Recursively remove all "geometry" fields, replacing them with geometry's size
        var filter = function (val, key) {
            if (key === 'geometry') {
                return val.length;
            } else if (_.isArray(val)) {
                return _.map(val, filter);
            } else if (_.isObject(val)) {
                _.each(val, function (v, k) {
                    val[k] = filter(v, k);
                });
            }
            return val;
        };
        data = _.map(data, filter);
    }
    return data;
}

/**
 * Web server (express) route handler to get requested tile, snapshot image, or info
 * @param req request object
 * @param res response object
 * @param next will be called if request is not handled
 */
function requestHandler(req, res, next) {

    var start = Date.now();
    // These vars might get set before finishing validation.
    // Do not use them unless successful
    var isStatic, srcId, source, opts, z, x, y, scale, format, handler;

    return BBPromise.try(function () {
        if (!sources) {
            throw new Err('The service has not started yet');
        }
        srcId = req.params.src;
        source = sources.getSourceById(srcId, true);
        if (!source) {
            throw new Err('Unknown source').metrics('err.req.source');
        }
        if (!source.public) {
            throw new Err('Source is not public').metrics('err.req.source');
        }
        var isInfoRequest = false;
        if (req.params.info) {
            if (req.params.info === 'pbfinfo' || req.params.info === 'info') {
                isInfoRequest = true;
                format = 'json';
            } else {
                throw new Err('Unexpected info type').metrics('err.req.info');
            }
        } else {
            format = req.params.format;
        }
        if (!isInfoRequest && format !== 'pbf' && !_.contains(source.formats, format)) {
            throw new Err('Format %s is not known', format).metrics('err.req.format');
        }
        if (format === 'pbf' || req.params.info === 'pbfinfo') {
            if (!source.pbfsource) {
                throw new Err('pbf access is not enabled for this source').metrics('err.req.pbf');
            }
            var pbfSrcId = source.pbfsource;
            source = sources.getSourceById(pbfSrcId);
            handler = sources.getHandlerById(pbfSrcId, true);
        } else {
            handler = sources.getHandlerById(srcId, true);
        }
        if (!handler) {
            throw new Err('The source has not started yet').metrics('err.req.source');
        }
        if (isInfoRequest) {
            return handler.getInfoAsync().then(function(info) {
                return [info, infoHeaders];
            });
        }

        z = core.strToInt(req.params.z);
        if (!core.isValidZoom(z)) {
            throw new Err('invalid zoom').metrics('err.req.coords');
        }
        if (source.minzoom !== undefined && z < source.minzoom) {
            throw new Err('Minimum zoom is %d', source.minzoom).metrics('err.req.zoom');
        }
        if (source.maxzoom !== undefined && z > source.maxzoom) {
            throw new Err('Maximum zoom is %d', source.maxzoom).metrics('err.req.zoom');
        }
        scale = req.params.scale;
        if (scale !== undefined) {
            if (!source.scales) {
                throw new Err('Scaling is not enabled for this source').metrics('err.req.scale');
            }
            if (!_.contains(source.scales, scale.toString())) {
                throw new Err('This scaling is not allowed for this source. Allowed: %s', source.scales.join())
                    .metrics('err.req.scale');
            }
            scale = parseFloat(scale);
        }

        isStatic = req.params.w !== undefined || req.params.h !== undefined;

        if (isStatic) {
            if (!source.static) {
                throw new Err('Static snapshot images are not enabled for this source').metrics('err.req.static');
            }
            if (format !== 'png' && format !== 'jpeg') {
                throw new Err('Format %s is not allowed for static images', format).metrics('err.req.stformat');
            }
            var lat = core.strToFloat(req.params.lat);
            var lon = core.strToFloat(req.params.lon);
            var w = core.strToInt(req.params.w);
            var h = core.strToInt(req.params.h);
            if (typeof lat !== 'number' || typeof lon !== 'number') {
                throw new Err('The lat and lon coordinates must be numeric for static images').metrics('err.req.stcoords');
            }
            if (!core.isInteger(w) || !core.isInteger(h)) {
                throw new Err('The width and height params must be integers for static images').metrics('err.req.stsize');
            }
            if (w > source.maxwidth || h > source.maxheight) {
                throw new Err('Requested image is too big').metrics('err.req.stsizebig');
            }
            var params = {
                zoom: z,
                scale: scale,
                center: {x: lon, y: lat, w: w, h: h},
                format: format,
                getTile: handler.getTile.bind(handler)
            };
            return abaculus(params);
        } else {
            x = core.strToInt(req.params.x);
            y = core.strToInt(req.params.y);
            if (!core.isValidCoordinate(x, z) || !core.isValidCoordinate(y, z)) {
                throw new Err('x,y coordinates are not valid, or not allowed for this zoom').metrics('err.req.coords');
            }
            if (format !== 'pbf') {
                opts = {format: format};
                if (scale) {
                    opts.scale = scale;
                }
            }
            return core.getTitleWithParamsAsync(handler, z, x, y, opts);
        }
    }).spread(function (data, dataHeaders) {
        if (app.conf.defaultHeaders) res.set(app.conf.defaultHeaders);
        if (source.defaultHeaders) res.set(source.defaultHeaders);
        if (dataHeaders) res.set(dataHeaders);
        if (app.conf.overrideHeaders) res.set(app.conf.overrideHeaders);
        if (source.headers) res.set(source.headers);

        if (format === 'json') {
            // Allow JSON to be shortened to simplify debugging
            res.json(filterJson(req.query, data));
        } else {
            res.send(data);
        }

        var mx = util.format('req.%s.%s', srcId, z);
        mx += '.' + format;
        if (isStatic) {
            mx += '.static';
        }
        if (scale) {
            // replace '.' with ',' -- otherwise grafana treats it as a divider
            mx += '.' + (scale.toString().replace('.', ','));
        }
        metrics.endTiming(mx, start);
    }).catch(reportRequestError).catch(next);
}

/**
 * Web server (express) route handler to get a marker icon
 * @param req request object
 * @param res response object
 * @param next will be called if request is not handled
 */
function markerHandler(req, res, next) {

    var start = Date.now(),
        params = req.params,
        metric = ['marker'];

    return BBPromise.try(function () {

        metric.push(params.base);
        metric.push(params.size);
        metric.push(params.symbol);

        if (params.color.length !== 3 && params.color.length !== 6) {
            throw new Err('Bad color').metrics('err.marker.color');
        }
        metric.push(params.color);

        var isRetina;
        if (params.scale === undefined) {
            isRetina = false;
        } else if (params.scale === '2') {
            metric.push(params.scale);
            isRetina = true;
        } else {
            throw new Err('Only retina @2x scaling is allowed for marks').metrics('err.marker.scale');
        }

        return makizushi({
            base: params.base, // "pin"
            size: params.size, // s|m|l
            symbol: params.symbol, // digit, letter, or maki symol name - https://www.mapbox.com/maki/
            tint: params.color, // in hex - "abc" or "aabbcc"
            retina: isRetina // true|false
        });
    }).then(function (data) {
        if (app.conf.defaultHeaders) res.set(app.conf.defaultHeaders);
        if (app.conf.overrideHeaders) res.set(app.conf.overrideHeaders);
        res.type('png').send(data);
        metrics.endTiming(metric.join('.'), start);
    }).catch(reportRequestError).catch(next);
}

module.exports.init = function(opts) {

    core = opts.core;
    Err = core.Err;
    app = opts.app;
    sources = opts.sources;
    metrics = app.metrics;

    var staticOpts = {};
    staticOpts.setHeaders = function (res) {
        if (app.conf.cache) {
            res.header('Cache-Control', app.conf.cache);
        }
        if (res.req.originalUrl.endsWith('.pbf')) {
            res.header('Content-Encoding', 'gzip');
        }
    };

    var router = express.Router();

    // get static image
    router.get('/img/:src(' + core.Sources.sourceIdReStr + '),:z(\\d+),:lat([-\\d\\.]+),:lon([-\\d\\.]+),:w(\\d+)x:h(\\d+).:format([\\w]+)', requestHandler);
    router.get('/img/:src(' + core.Sources.sourceIdReStr + '),:z(\\d+),:lat([-\\d\\.]+),:lon([-\\d\\.]+),:w(\\d+)x:h(\\d+)@:scale([\\.\\d]+)x.:format([\\w]+)', requestHandler);

    // marker icon generator  (base, size, symbol, color, scale)
    // /marker/pin-m-cafe-7e7e7e@2x.png
    router.get('/marker/:base([\\w]+),:size([sml]),:symbol([-\\w]+),:color([a-f0-9]+).png', markerHandler);
    router.get('/marker/:base([\\w]+),:size([sml]),:symbol([-\\w]+),:color([a-f0-9]+)@:scale([\\.\\d]+)x.png', markerHandler);

    // get tile
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w]+)', requestHandler);
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:z(\\d+)/:x(\\d+)/:y(\\d+)@:scale([\\.\\d]+)x.:format([\\w]+)', requestHandler);

    // get source info (json)
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:info(pbfinfo).json', requestHandler);
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:info(info).json', requestHandler);

    // Add before static to prevent disk IO on each tile request
    app.use('/', router);
    app.use('/', express.static(pathLib.resolve(__dirname, '../static'), staticOpts));
    app.use('/leaflet', express.static(pathLib.dirname(require.resolve('leaflet')), staticOpts));

    metrics.increment('init');
};
<commit_msg>Fixed error reporting bug<commit_after>'use strict';

var util = require('util');
var pathLib = require('path');
var BBPromise = require('bluebird');
var _ = require('underscore');
var express = require('express');

var abaculus = BBPromise.promisify(require('abaculus'));
var makizushi = BBPromise.promisify(require('makizushi'));

var core, Err;
var app, sources;
var metrics;

var infoHeaders = {};

function reportError(errReporterFunc, err) {
    try {
        errReporterFunc(err);
    } catch (e2) {
        console.error('Unable to report: ' + core.errToStr(err) + '\n\nDue to: ' + core.errToStr(e2));
    }
}

function reportRequestError(err, res) {
    reportError(function (err) {
        res
            .status(400)
            .header('Cache-Control', 'public, s-maxage=30, max-age=30')
            .json(err.message || 'error/unknown');
        core.log('error', err);
        metrics.increment(err.metrics || 'err.unknown');
    }, err);
}

function filterJson(query, data) {
    if ('summary' in query) {
        data = _(data).reduce(function (memo, layer) {
            memo[layer.name] = {
                features: layer.features.length,
                jsonsize: JSON.stringify(layer).length
            };
            return memo;
        }, {});
    } else if ('nogeo' in query) {
        // Recursively remove all "geometry" fields, replacing them with geometry's size
        var filter = function (val, key) {
            if (key === 'geometry') {
                return val.length;
            } else if (_.isArray(val)) {
                return _.map(val, filter);
            } else if (_.isObject(val)) {
                _.each(val, function (v, k) {
                    val[k] = filter(v, k);
                });
            }
            return val;
        };
        data = _.map(data, filter);
    }
    return data;
}

/**
 * Web server (express) route handler to get requested tile, snapshot image, or info
 * @param req request object
 * @param res response object
 * @param next will be called if request is not handled
 */
function requestHandler(req, res, next) {

    var start = Date.now();
    // These vars might get set before finishing validation.
    // Do not use them unless successful
    var isStatic, srcId, source, opts, z, x, y, scale, format, handler;

    return BBPromise.try(function () {
        if (!sources) {
            throw new Err('The service has not started yet');
        }
        srcId = req.params.src;
        source = sources.getSourceById(srcId, true);
        if (!source) {
            throw new Err('Unknown source').metrics('err.req.source');
        }
        if (!source.public) {
            throw new Err('Source is not public').metrics('err.req.source');
        }
        var isInfoRequest = false;
        if (req.params.info) {
            if (req.params.info === 'pbfinfo' || req.params.info === 'info') {
                isInfoRequest = true;
                format = 'json';
            } else {
                throw new Err('Unexpected info type').metrics('err.req.info');
            }
        } else {
            format = req.params.format;
        }
        if (!isInfoRequest && format !== 'pbf' && !_.contains(source.formats, format)) {
            throw new Err('Format %s is not known', format).metrics('err.req.format');
        }
        if (format === 'pbf' || req.params.info === 'pbfinfo') {
            if (!source.pbfsource) {
                throw new Err('pbf access is not enabled for this source').metrics('err.req.pbf');
            }
            var pbfSrcId = source.pbfsource;
            source = sources.getSourceById(pbfSrcId);
            handler = sources.getHandlerById(pbfSrcId, true);
        } else {
            handler = sources.getHandlerById(srcId, true);
        }
        if (!handler) {
            throw new Err('The source has not started yet').metrics('err.req.source');
        }
        if (isInfoRequest) {
            return handler.getInfoAsync().then(function(info) {
                return [info, infoHeaders];
            });
        }

        z = core.strToInt(req.params.z);
        if (!core.isValidZoom(z)) {
            throw new Err('invalid zoom').metrics('err.req.coords');
        }
        if (source.minzoom !== undefined && z < source.minzoom) {
            throw new Err('Minimum zoom is %d', source.minzoom).metrics('err.req.zoom');
        }
        if (source.maxzoom !== undefined && z > source.maxzoom) {
            throw new Err('Maximum zoom is %d', source.maxzoom).metrics('err.req.zoom');
        }
        scale = req.params.scale;
        if (scale !== undefined) {
            if (!source.scales) {
                throw new Err('Scaling is not enabled for this source').metrics('err.req.scale');
            }
            if (!_.contains(source.scales, scale.toString())) {
                throw new Err('This scaling is not allowed for this source. Allowed: %s', source.scales.join())
                    .metrics('err.req.scale');
            }
            scale = parseFloat(scale);
        }

        isStatic = req.params.w !== undefined || req.params.h !== undefined;

        if (isStatic) {
            if (!source.static) {
                throw new Err('Static snapshot images are not enabled for this source').metrics('err.req.static');
            }
            if (format !== 'png' && format !== 'jpeg') {
                throw new Err('Format %s is not allowed for static images', format).metrics('err.req.stformat');
            }
            var lat = core.strToFloat(req.params.lat);
            var lon = core.strToFloat(req.params.lon);
            var w = core.strToInt(req.params.w);
            var h = core.strToInt(req.params.h);
            if (typeof lat !== 'number' || typeof lon !== 'number') {
                throw new Err('The lat and lon coordinates must be numeric for static images').metrics('err.req.stcoords');
            }
            if (!core.isInteger(w) || !core.isInteger(h)) {
                throw new Err('The width and height params must be integers for static images').metrics('err.req.stsize');
            }
            if (w > source.maxwidth || h > source.maxheight) {
                throw new Err('Requested image is too big').metrics('err.req.stsizebig');
            }
            var params = {
                zoom: z,
                scale: scale,
                center: {x: lon, y: lat, w: w, h: h},
                format: format,
                getTile: handler.getTile.bind(handler)
            };
            return abaculus(params);
        } else {
            x = core.strToInt(req.params.x);
            y = core.strToInt(req.params.y);
            if (!core.isValidCoordinate(x, z) || !core.isValidCoordinate(y, z)) {
                throw new Err('x,y coordinates are not valid, or not allowed for this zoom').metrics('err.req.coords');
            }
            if (format !== 'pbf') {
                opts = {format: format};
                if (scale) {
                    opts.scale = scale;
                }
            }
            return core.getTitleWithParamsAsync(handler, z, x, y, opts);
        }
    }).spread(function (data, dataHeaders) {
        if (app.conf.defaultHeaders) res.set(app.conf.defaultHeaders);
        if (source.defaultHeaders) res.set(source.defaultHeaders);
        if (dataHeaders) res.set(dataHeaders);
        if (app.conf.overrideHeaders) res.set(app.conf.overrideHeaders);
        if (source.headers) res.set(source.headers);

        if (format === 'json') {
            // Allow JSON to be shortened to simplify debugging
            res.json(filterJson(req.query, data));
        } else {
            res.send(data);
        }

        var mx = util.format('req.%s.%s', srcId, z);
        mx += '.' + format;
        if (isStatic) {
            mx += '.static';
        }
        if (scale) {
            // replace '.' with ',' -- otherwise grafana treats it as a divider
            mx += '.' + (scale.toString().replace('.', ','));
        }
        metrics.endTiming(mx, start);
    }).catch(function(err) {
        return reportRequestError(err, res);
    }).catch(next);
}

/**
 * Web server (express) route handler to get a marker icon
 * @param req request object
 * @param res response object
 * @param next will be called if request is not handled
 */
function markerHandler(req, res, next) {

    var start = Date.now(),
        params = req.params,
        metric = ['marker'];

    return BBPromise.try(function () {

        metric.push(params.base);
        metric.push(params.size);
        metric.push(params.symbol);

        if (params.color.length !== 3 && params.color.length !== 6) {
            throw new Err('Bad color').metrics('err.marker.color');
        }
        metric.push(params.color);

        var isRetina;
        if (params.scale === undefined) {
            isRetina = false;
        } else if (params.scale === '2') {
            metric.push(params.scale);
            isRetina = true;
        } else {
            throw new Err('Only retina @2x scaling is allowed for marks').metrics('err.marker.scale');
        }

        return makizushi({
            base: params.base, // "pin"
            size: params.size, // s|m|l
            symbol: params.symbol, // digit, letter, or maki symol name - https://www.mapbox.com/maki/
            tint: params.color, // in hex - "abc" or "aabbcc"
            retina: isRetina // true|false
        });
    }).then(function (data) {
        if (app.conf.defaultHeaders) res.set(app.conf.defaultHeaders);
        if (app.conf.overrideHeaders) res.set(app.conf.overrideHeaders);
        res.type('png').send(data);
        metrics.endTiming(metric.join('.'), start);
    }).catch(function(err) {
        return reportRequestError(err, res);
    }).catch(next);
}

module.exports.init = function(opts) {

    core = opts.core;
    Err = core.Err;
    app = opts.app;
    sources = opts.sources;
    metrics = app.metrics;

    var staticOpts = {};
    staticOpts.setHeaders = function (res) {
        if (app.conf.cache) {
            res.header('Cache-Control', app.conf.cache);
        }
        if (res.req.originalUrl.endsWith('.pbf')) {
            res.header('Content-Encoding', 'gzip');
        }
    };

    var router = express.Router();

    // get static image
    router.get('/img/:src(' + core.Sources.sourceIdReStr + '),:z(\\d+),:lat([-\\d\\.]+),:lon([-\\d\\.]+),:w(\\d+)x:h(\\d+).:format([\\w]+)', requestHandler);
    router.get('/img/:src(' + core.Sources.sourceIdReStr + '),:z(\\d+),:lat([-\\d\\.]+),:lon([-\\d\\.]+),:w(\\d+)x:h(\\d+)@:scale([\\.\\d]+)x.:format([\\w]+)', requestHandler);

    // marker icon generator  (base, size, symbol, color, scale)
    // /marker/pin-m-cafe-7e7e7e@2x.png
    router.get('/marker/:base([\\w]+),:size([sml]),:symbol([-\\w]+),:color([a-f0-9]+).png', markerHandler);
    router.get('/marker/:base([\\w]+),:size([sml]),:symbol([-\\w]+),:color([a-f0-9]+)@:scale([\\.\\d]+)x.png', markerHandler);

    // get tile
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:z(\\d+)/:x(\\d+)/:y(\\d+).:format([\\w]+)', requestHandler);
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:z(\\d+)/:x(\\d+)/:y(\\d+)@:scale([\\.\\d]+)x.:format([\\w]+)', requestHandler);

    // get source info (json)
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:info(pbfinfo).json', requestHandler);
    router.get('/:src(' + core.Sources.sourceIdReStr + ')/:info(info).json', requestHandler);

    // Add before static to prevent disk IO on each tile request
    app.use('/', router);
    app.use('/', express.static(pathLib.resolve(__dirname, '../static'), staticOpts));
    app.use('/leaflet', express.static(pathLib.dirname(require.resolve('leaflet')), staticOpts));

    metrics.increment('init');
};
<|endoftext|>


<commit_before>/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file Static data manager for HRIR list and URLs. For Omnitone's HRIR list
 * structure, see src/resources/README.md for the detail.
 */

'use strict';

const HRIRList = [
  // Zero-order ambisonic. Not supported. (0 files, 1 channel)
  null,
  // First-order ambisonic. (2 files, 4 channels)
  ['omnitone-foa-1.wav', 'omnitone-foa-2.wav'],
  // Second-order ambisonic. (5 files, 9 channels)
  [
    'omnitone-soa-1.wav', 'omnitone-soa-2.wav', 'omnitone-soa-3.wav',
    'omnitone-soa-4.wav', 'omnitone-soa-5.wav',
  ],
  // Third-order ambisonic. (8 files, 16 channels)
  [
    'omnitone-toa-1.wav', 'omnitone-toa-2.wav', 'omnitone-toa-3.wav',
    'omnitone-toa-4.wav', 'omnitone-toa-5.wav', 'omnitone-toa-6.wav',
    'omnitone-toa-7.wav', 'omnitone-toa-8.wav',
  ],
];

// Base URL.
const SourceURL = {
  GITHUB:
      'https://cdn.rawgit.com/GoogleChrome/omnitone/master/build/resources/',
};

/**
 * [getPathSet description]
 * @param {object} [setting] - Setting object.
 * @param {string} [setting.source="github"] - The base location for the HRIR
 * set.
 * @param {number} [setting.ambisonicOrder=1] - Requested ambisonic order.
 * @return {string[]} pathList - HRIR path set (2~8 URLs)
 */
module.exports.getPathList = function(setting) {
  let filenames;
  let staticPath;
  let pathList;

  const setting_ = setting || {ambisonicOrder: 1, source: 'github'};

  switch (setting_.ambisonicOrder) {
    case 1:
    case 2:
    case 3:
      filenames = HRIRList[setting_.ambisonicOrder];
      break;
    default:
      // Invalid order gets the null path list.
      filenames = HRIRList[0];
      break;
  }

  switch (setting_.source) {
    case 'github':
    default:
      // By default, use GitHub's CDN.
      staticPath = SourceURL.GITHUB;
      break;
  }

  if (filenames) {
    pathList = [];
    filenames.forEach(function(filename) {
      pathList.push(staticPath + filename);
    });
  }

  return pathList;
};
<commit_msg>eslint fix<commit_after>/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @file Static data manager for HRIR list and URLs. For Omnitone's HRIR list
 * structure, see src/resources/README.md for the detail.
 */

'use strict';

const HRIRList = [
  // Zero-order ambisonic. Not supported. (0 files, 1 channel)
  null,
  // First-order ambisonic. (2 files, 4 channels)
  ['omnitone-foa-1.wav', 'omnitone-foa-2.wav'],
  // Second-order ambisonic. (5 files, 9 channels)
  [
    'omnitone-soa-1.wav', 'omnitone-soa-2.wav', 'omnitone-soa-3.wav',
    'omnitone-soa-4.wav', 'omnitone-soa-5.wav',
  ],
  // Third-order ambisonic. (8 files, 16 channels)
  [
    'omnitone-toa-1.wav', 'omnitone-toa-2.wav', 'omnitone-toa-3.wav',
    'omnitone-toa-4.wav', 'omnitone-toa-5.wav', 'omnitone-toa-6.wav',
    'omnitone-toa-7.wav', 'omnitone-toa-8.wav',
  ],
];

// Base URL.
const SourceURL = {
  GITHUB:
      'https://cdn.rawgit.com/GoogleChrome/omnitone/master/build/resources/',
};

/**
 * [getPathSet description]
 * @param {object} [setting] - Setting object.
 * @param {string} [setting.source="github"] - The base location for the HRIR
 * set.
 * @param {number} [setting.ambisonicOrder=1] - Requested ambisonic order.
 * @return {string[]} pathList - HRIR path set (2~8 URLs)
 */
module.exports.getPathList = function(setting) {
  let filenames;
  let staticPath;
  let pathList;

  const setting_ = setting || {ambisonicOrder: 1, source: 'github'};

  switch (setting_.ambisonicOrder) {
    case 1:
    case 2:
    case 3:
      filenames = HRIRList[setting_.ambisonicOrder];
      break;
    default:
      // Invalid order gets the null path list.
      filenames = HRIRList[0];
      break;
  }

  switch (setting_.source) {
    case 'github':
    default:
      // By default, use GitHub's CDN.
      staticPath = SourceURL.GITHUB;
      break;
  }

  if (filenames) {
    pathList = [];
    filenames.forEach(function(filename) {
      pathList.push(staticPath + filename);
    });
  }

  return pathList;
};
<|endoftext|>


<commit_before>/* Copyright (c) 2006 MetaCarta, Inc., published under a modified BSD license.
 * See http://svn.openlayers.org/trunk/openlayers/repository-license.txt
 * for the full text of the license. */

/**
 * @class
 *
 * @requires OpenLayers/Control.js
 * @requires OpenLayers/Control.js
 */
OpenLayers.Control.LayerSwitcher = OpenLayers.Class.create();
OpenLayers.Control.LayerSwitcher.prototype =
  OpenLayers.Class.inherit( OpenLayers.Control, {

    /** @type String */
    activeColor: "darkblue",

  // DOM Elements

    /** @type DOMElement */
    layersDiv: null,

    /** @type DOMElement */
    baseLayersDiv: null,

    /** @type Array */
    baseLayers: null,

    /** @type DOMElement */
    dataLbl: null,

    /** @type DOMElement */
    dataLayersDiv: null,

    /** @type Array */
    dataLayers: null,

    /** @type DOMElement */
    minimizeDiv: null,

    /** @type DOMElement */
    maximizeDiv: null,

    /** @type Boolean */
    ascending: true,

    /**
    * @constructor
    */
    initialize: function(options) {
        OpenLayers.Control.prototype.initialize.apply(this, arguments);
    },

    /**
     *
     */
    destroy: function() {

        OpenLayers.Event.stopObservingElement(this.div);

        OpenLayers.Event.stopObservingElement(this.minimizeDiv);
        OpenLayers.Event.stopObservingElement(this.maximizeDiv);

        //clear out layers info and unregister their events
        this.clearLayersArray("base");
        this.clearLayersArray("data");

        this.map.events.unregister("addlayer", this, this.redraw);
        this.map.events.unregister("changelayer", this, this.redraw);
        this.map.events.unregister("removelayer", this, this.redraw);
        this.map.events.unregister("changebaselayer", this, this.redraw);

        OpenLayers.Control.prototype.destroy.apply(this, arguments);
    },

    /**
     * @param {OpenLayers.Map} map
     */
    setMap: function(map) {
        OpenLayers.Control.prototype.setMap.apply(this, arguments);

        this.map.events.register("addlayer", this, this.redraw);
        this.map.events.register("changelayer", this, this.redraw);
        this.map.events.register("removelayer", this, this.redraw);
        this.map.events.register("changebaselayer", this, this.redraw);
    },

    /**
    * @returns A reference to the DIV DOMElement containing the switcher tabs
    * @type DOMElement
    */
    draw: function() {
        OpenLayers.Control.prototype.draw.apply(this);

        // create layout divs
        this.loadContents();

        // set mode to minimize
        this.minimizeControl();

        // populate div with current info
        this.redraw();

        return this.div;
    },

    /** user specifies either "base" or "data". we then clear all the
     *    corresponding listeners, the div, and reinitialize a new array.
     *
     * @private
     *
     * @param {String} layersType Ei
     */
    clearLayersArray: function(layersType) {
        var layers = this[layersType + "Layers"];
        if (layers) {
            for(var i=0; i < layers.length; i++) {
                var layer = layers[i];
                OpenLayers.Event.stopObservingElement(layer.inputElem);
                OpenLayers.Event.stopObservingElement(layer.labelSpan);
            }
        }
        this[layersType + "LayersDiv"].innerHTML = "";
        this[layersType + "Layers"] = new Array();
    },

    /** Goes through and takes the current state of the Map and rebuilds the
     *   control to display that state. Groups base layers into a radio-button
     *   group and lists each data layer with a checkbox.
     *
     * @returns A reference to the DIV DOMElement containing the control
     * @type DOMElement
     */
    redraw: function() {

        //clear out previous layers
        this.clearLayersArray("base");
        this.clearLayersArray("data");

        var containsOverlays = false;

        var layers = this.map.layers.slice();
        if (!this.ascending) { layers.reverse(); }
        for( var i = 0; i < layers.length; i++) {
            var layer = layers[i];
            var baseLayer = layer.isBaseLayer;

            if (baseLayer || layer.displayInLayerSwitcher) {

                if (!baseLayer) {
                    containsOverlays = true;
                }

                // only check a baselayer if it is *the* baselayer, check data
                //  layers if they are visible
                var checked = (baseLayer) ? (layer == this.map.baseLayer)
                                          : layer.getVisibility();

                // create input element
                var inputElem = document.createElement("input");
                inputElem.id = "input_" + layer.name;
                inputElem.name = (baseLayer) ? "baseLayers" : layer.name;
                inputElem.type = (baseLayer) ? "radio" : "checkbox";
                inputElem.value = layer.name;
                inputElem.checked = checked;
                inputElem.defaultChecked = checked;

                if (!baseLayer && !layer.inRange) {
                    inputElem.disabled = true;
                }
                var context = {
                    'inputElem': inputElem,
                    'layer': layer,
                    'layerSwitcher': this
                }
                OpenLayers.Event.observe(inputElem, "mouseup",
                              this.onInputClick.bindAsEventListener(context));

                // create span
                var labelSpan = document.createElement("span");
                if (!baseLayer && !layer.inRange) {
                    labelSpan.style.color = "gray";
                }
                labelSpan.innerHTML = layer.name;
                labelSpan.style.verticalAlign = (baseLayer) ? "bottom"
                                                            : "baseline";
                OpenLayers.Event.observe(labelSpan, "click",
                              this.onInputClick.bindAsEventListener(context));
                // create line break
                var br = document.createElement("br");

                var groupArray = (baseLayer) ? this.baseLayers
                                             : this.dataLayers;
                groupArray.push({
                    'layer': layer,
                    'inputElem': inputElem,
                    'labelSpan': labelSpan
                });

                var groupDiv = (baseLayer) ? this.baseLayersDiv
                                           : this.dataLayersDiv;
                groupDiv.appendChild(inputElem);
                groupDiv.appendChild(labelSpan);
                groupDiv.appendChild(br);
            }
        }

        // if no overlays, dont display the overlay label
        this.dataLbl.style.display = (containsOverlays) ? "" : "none";

        return this.div;
    },

    /** A label has been clicked, check or uncheck its corresponding input
     *
     * @private
     *
     * @context
     *      {DOMElement} inputElem
     *      {OpenLayers.Control.LayerSwitcher} layerSwitcher
     *      {OpenLayers.Layer} layer
     *
     * @param {Event} e
     */

    onInputClick: function(e) {

        if (!this.inputElem.disabled) {
            if (this.inputElem.type == "radio") {
                this.inputElem.checked = true;
                this.layer.map.setBaseLayer(this.layer, true);
                this.layer.map.events.triggerEvent("changebaselayer");
            } else {
                this.inputElem.checked = !this.inputElem.checked;
                this.layerSwitcher.updateMap();
            }
        }
        OpenLayers.Event.stop(e);
    },

    /** Need to update the map accordingly whenever user clicks in either of
     *   the layers.
     *
     * @private
     *
     * @param {Event} e
     */
    onLayerClick: function(e) {
        this.updateMap();
    },

    /** Cycles through the loaded data and base layer input arrays and makes
     *   the necessary calls to the Map object such that that the map's
     *   visual state corresponds to what the user has selected in the control
     *
     * @private
     */
    updateMap: function() {

        // set the newly selected base layer
        for(var i=0; i < this.baseLayers.length; i++) {
            var layerEntry = this.baseLayers[i];
            if (layerEntry.inputElem.checked) {
                this.map.setBaseLayer(layerEntry.layer, false);
            }
        }

        // set the correct visibilities for the overlays
        for(var i=0; i < this.dataLayers.length; i++) {
            var layerEntry = this.dataLayers[i];
            layerEntry.layer.setVisibility(layerEntry.inputElem.checked, true);
        }

    },

    /** Set up the labels and divs for the control
     *
     * @param {Event} e
     */
    maximizeControl: function(e) {

        //HACK HACK HACK - find a way to auto-size this layerswitcher
        this.div.style.width = "20em";
        this.div.style.height = "";

        this.showControls(false);

        if (e != null) {
            OpenLayers.Event.stop(e);
        }
    },

    /** Hide all the contents of the control, shrink the size,
     *   add the maximize icon
     *
     * @param {Event} e
     */
    minimizeControl: function(e) {

        this.div.style.width = "0px";
        this.div.style.height = "0px";

        this.showControls(true);

        if (e != null) {
            OpenLayers.Event.stop(e);
        }
    },

    /** Hide/Show all LayerSwitcher controls depending on whether we are
     *   minimized or not
     *
     * @private
     *
     * @param {Boolean} minimize
     */
    showControls: function(minimize) {

        this.maximizeDiv.style.display = minimize ? "" : "none";
        this.minimizeDiv.style.display = minimize ? "none" : "";

        this.layersDiv.style.display = minimize ? "none" : "";
    },

    /** Set up the labels and divs for the control
     *
     */
    loadContents: function() {

        //configure main div
        this.div.style.position = "absolute";
        this.div.style.top = "10px";
        this.div.style.right = "0px";
        this.div.style.left = "";
        this.div.style.fontFamily = "sans-serif";
        this.div.style.fontWeight = "bold";
        this.div.style.marginTop = "3px";
        this.div.style.marginLeft = "3px";
        this.div.style.marginBottom = "3px";
        this.div.style.fontSize = "smaller";
        this.div.style.color = "white";
        this.div.style.backgroundColor = "transparent";

        OpenLayers.Event.observe(this.div, "mouseup",
                      this.mouseUp.bindAsEventListener(this));
        OpenLayers.Event.observe(this.div, "click",
                      this.ignoreEvent);
        OpenLayers.Event.observe(this.div, "mousedown",
                      this.mouseDown.bindAsEventListener(this));
        OpenLayers.Event.observe(this.div, "dblclick", this.ignoreEvent);

        // layers list div
        this.layersDiv = document.createElement("div");
        this.layersDiv.id = "layersDiv";
        this.layersDiv.style.paddingTop = "5px";
        this.layersDiv.style.paddingLeft = "10px";
        this.layersDiv.style.paddingBottom = "5px";
        this.layersDiv.style.paddingRight = "75px";
        this.layersDiv.style.backgroundColor = this.activeColor;

        // had to set width/height to get transparency in IE to work.
        // thanks -- http://jszen.blogspot.com/2005/04/ie6-opacity-filter-caveat.html
        //
        this.layersDiv.style.width = "100%";
        this.layersDiv.style.height = "100%";

        var baseLbl = document.createElement("div");
        baseLbl.innerHTML = "Base Layer";
        baseLbl.style.marginTop = "3px";
        baseLbl.style.marginLeft = "3px";
        baseLbl.style.marginBottom = "3px";

        this.baseLayersDiv = document.createElement("div");
        this.baseLayersDiv.style.paddingLeft = "10px";
        /*OpenLayers.Event.observe(this.baseLayersDiv, "click",
                      this.onLayerClick.bindAsEventListener(this));
        */

        this.dataLbl = document.createElement("div");
        this.dataLbl.innerHTML = "Overlays";
        this.dataLbl.style.marginTop = "3px";
        this.dataLbl.style.marginLeft = "3px";
        this.dataLbl.style.marginBottom = "3px";

        this.dataLayersDiv = document.createElement("div");
        this.dataLayersDiv.style.paddingLeft = "10px";

        if (this.ascending) {
            this.layersDiv.appendChild(baseLbl);
            this.layersDiv.appendChild(this.baseLayersDiv);
            this.layersDiv.appendChild(this.dataLbl);
            this.layersDiv.appendChild(this.dataLayersDiv);
        } else {
            this.layersDiv.appendChild(this.dataLbl);
            this.layersDiv.appendChild(this.dataLayersDiv);
            this.layersDiv.appendChild(baseLbl);
            this.layersDiv.appendChild(this.baseLayersDiv);
        }

        this.div.appendChild(this.layersDiv);

        OpenLayers.Rico.Corner.round(this.div, {corners: "tl bl",
                                        bgColor: "transparent",
                                        color: this.activeColor,
                                        blend: false});

        OpenLayers.Rico.Corner.changeOpacity(this.layersDiv, 0.75);

        var imgLocation = OpenLayers.Util.getImagesLocation();
        var sz = new OpenLayers.Size(18,18);

        // maximize button div
        var img = imgLocation + 'layer-switcher-maximize.png';
        this.maximizeDiv = OpenLayers.Util.createAlphaImageDiv(
                                    "OpenLayers_Control_MaximizeDiv",
                                    null,
                                    sz,
                                    img,
                                    "absolute");
        this.maximizeDiv.style.top = "5px";
        this.maximizeDiv.style.right = "0px";
        this.maximizeDiv.style.left = "";
        this.maximizeDiv.style.display = "none";
        OpenLayers.Event.observe(this.maximizeDiv,
                      "click",
                      this.maximizeControl.bindAsEventListener(this));

        this.div.appendChild(this.maximizeDiv);

        // minimize button div
        var img = imgLocation + 'layer-switcher-minimize.png';
        var sz = new OpenLayers.Size(18,18);
        this.minimizeDiv = OpenLayers.Util.createAlphaImageDiv(
                                    "OpenLayers_Control_MinimizeDiv",
                                    null,
                                    sz,
                                    img,
                                    "absolute");
        this.minimizeDiv.style.top = "5px";
        this.minimizeDiv.style.right = "0px";
        this.minimizeDiv.style.left = "";
        this.minimizeDiv.style.display = "none";
        OpenLayers.Event.observe(this.minimizeDiv,
                      "click",
                      this.minimizeControl.bindAsEventListener(this));

        this.div.appendChild(this.minimizeDiv);
    },

    /**
     * @private
     *
     * @param {Event} evt
     */
    ignoreEvent: function(evt) {
        OpenLayers.Event.stop(evt);
    },

    /** Register a local 'mouseDown' flag so that we'll know whether or not
     *   to ignore a mouseUp event
     *
     * @private
     *
     * @param {Event} evt
     */
    mouseDown: function(evt) {
        this.mouseDown = true;
        this.ignoreEvent(evt);
    },

    /** If the 'mouseDown' flag has been set, that means that the drag was
     *   started from within the LayerSwitcher control, and thus we can
     *   ignore the mouseup. Otherwise, let the Event continue.
     *
     * @private
     *
     * @param {Event} evt
     */
    mouseUp: function(evt) {
        if (this.mouseDown) {
            this.mouseDown = false;
            this.ignoreEvent(evt);
        }
    },

    /** @final @type String */
    CLASS_NAME: "OpenLayers.Control.LayerSwitcher"
});
<commit_msg>Move LayerSwitcher control down so that it doesn't overlay with the editing  controls. Patch from #641 by penyaskito . Thanks!<commit_after>/* Copyright (c) 2006 MetaCarta, Inc., published under a modified BSD license.
 * See http://svn.openlayers.org/trunk/openlayers/repository-license.txt
 * for the full text of the license. */

/**
 * @class
 *
 * @requires OpenLayers/Control.js
 * @requires OpenLayers/Control.js
 */
OpenLayers.Control.LayerSwitcher = OpenLayers.Class.create();
OpenLayers.Control.LayerSwitcher.prototype =
  OpenLayers.Class.inherit( OpenLayers.Control, {

    /** @type String */
    activeColor: "darkblue",

  // DOM Elements

    /** @type DOMElement */
    layersDiv: null,

    /** @type DOMElement */
    baseLayersDiv: null,

    /** @type Array */
    baseLayers: null,

    /** @type DOMElement */
    dataLbl: null,

    /** @type DOMElement */
    dataLayersDiv: null,

    /** @type Array */
    dataLayers: null,

    /** @type DOMElement */
    minimizeDiv: null,

    /** @type DOMElement */
    maximizeDiv: null,

    /** @type Boolean */
    ascending: true,

    /**
    * @constructor
    */
    initialize: function(options) {
        OpenLayers.Control.prototype.initialize.apply(this, arguments);
    },

    /**
     *
     */
    destroy: function() {

        OpenLayers.Event.stopObservingElement(this.div);

        OpenLayers.Event.stopObservingElement(this.minimizeDiv);
        OpenLayers.Event.stopObservingElement(this.maximizeDiv);

        //clear out layers info and unregister their events
        this.clearLayersArray("base");
        this.clearLayersArray("data");

        this.map.events.unregister("addlayer", this, this.redraw);
        this.map.events.unregister("changelayer", this, this.redraw);
        this.map.events.unregister("removelayer", this, this.redraw);
        this.map.events.unregister("changebaselayer", this, this.redraw);

        OpenLayers.Control.prototype.destroy.apply(this, arguments);
    },

    /**
     * @param {OpenLayers.Map} map
     */
    setMap: function(map) {
        OpenLayers.Control.prototype.setMap.apply(this, arguments);

        this.map.events.register("addlayer", this, this.redraw);
        this.map.events.register("changelayer", this, this.redraw);
        this.map.events.register("removelayer", this, this.redraw);
        this.map.events.register("changebaselayer", this, this.redraw);
    },

    /**
    * @returns A reference to the DIV DOMElement containing the switcher tabs
    * @type DOMElement
    */
    draw: function() {
        OpenLayers.Control.prototype.draw.apply(this);

        // create layout divs
        this.loadContents();

        // set mode to minimize
        this.minimizeControl();

        // populate div with current info
        this.redraw();

        return this.div;
    },

    /** user specifies either "base" or "data". we then clear all the
     *    corresponding listeners, the div, and reinitialize a new array.
     *
     * @private
     *
     * @param {String} layersType Ei
     */
    clearLayersArray: function(layersType) {
        var layers = this[layersType + "Layers"];
        if (layers) {
            for(var i=0; i < layers.length; i++) {
                var layer = layers[i];
                OpenLayers.Event.stopObservingElement(layer.inputElem);
                OpenLayers.Event.stopObservingElement(layer.labelSpan);
            }
        }
        this[layersType + "LayersDiv"].innerHTML = "";
        this[layersType + "Layers"] = new Array();
    },

    /** Goes through and takes the current state of the Map and rebuilds the
     *   control to display that state. Groups base layers into a radio-button
     *   group and lists each data layer with a checkbox.
     *
     * @returns A reference to the DIV DOMElement containing the control
     * @type DOMElement
     */
    redraw: function() {

        //clear out previous layers
        this.clearLayersArray("base");
        this.clearLayersArray("data");

        var containsOverlays = false;

        var layers = this.map.layers.slice();
        if (!this.ascending) { layers.reverse(); }
        for( var i = 0; i < layers.length; i++) {
            var layer = layers[i];
            var baseLayer = layer.isBaseLayer;

            if (baseLayer || layer.displayInLayerSwitcher) {

                if (!baseLayer) {
                    containsOverlays = true;
                }

                // only check a baselayer if it is *the* baselayer, check data
                //  layers if they are visible
                var checked = (baseLayer) ? (layer == this.map.baseLayer)
                                          : layer.getVisibility();

                // create input element
                var inputElem = document.createElement("input");
                inputElem.id = "input_" + layer.name;
                inputElem.name = (baseLayer) ? "baseLayers" : layer.name;
                inputElem.type = (baseLayer) ? "radio" : "checkbox";
                inputElem.value = layer.name;
                inputElem.checked = checked;
                inputElem.defaultChecked = checked;

                if (!baseLayer && !layer.inRange) {
                    inputElem.disabled = true;
                }
                var context = {
                    'inputElem': inputElem,
                    'layer': layer,
                    'layerSwitcher': this
                }
                OpenLayers.Event.observe(inputElem, "mouseup",
                              this.onInputClick.bindAsEventListener(context));

                // create span
                var labelSpan = document.createElement("span");
                if (!baseLayer && !layer.inRange) {
                    labelSpan.style.color = "gray";
                }
                labelSpan.innerHTML = layer.name;
                labelSpan.style.verticalAlign = (baseLayer) ? "bottom"
                                                            : "baseline";
                OpenLayers.Event.observe(labelSpan, "click",
                              this.onInputClick.bindAsEventListener(context));
                // create line break
                var br = document.createElement("br");

                var groupArray = (baseLayer) ? this.baseLayers
                                             : this.dataLayers;
                groupArray.push({
                    'layer': layer,
                    'inputElem': inputElem,
                    'labelSpan': labelSpan
                });

                var groupDiv = (baseLayer) ? this.baseLayersDiv
                                           : this.dataLayersDiv;
                groupDiv.appendChild(inputElem);
                groupDiv.appendChild(labelSpan);
                groupDiv.appendChild(br);
            }
        }

        // if no overlays, dont display the overlay label
        this.dataLbl.style.display = (containsOverlays) ? "" : "none";

        return this.div;
    },

    /** A label has been clicked, check or uncheck its corresponding input
     *
     * @private
     *
     * @context
     *      {DOMElement} inputElem
     *      {OpenLayers.Control.LayerSwitcher} layerSwitcher
     *      {OpenLayers.Layer} layer
     *
     * @param {Event} e
     */

    onInputClick: function(e) {

        if (!this.inputElem.disabled) {
            if (this.inputElem.type == "radio") {
                this.inputElem.checked = true;
                this.layer.map.setBaseLayer(this.layer, true);
                this.layer.map.events.triggerEvent("changebaselayer");
            } else {
                this.inputElem.checked = !this.inputElem.checked;
                this.layerSwitcher.updateMap();
            }
        }
        OpenLayers.Event.stop(e);
    },

    /** Need to update the map accordingly whenever user clicks in either of
     *   the layers.
     *
     * @private
     *
     * @param {Event} e
     */
    onLayerClick: function(e) {
        this.updateMap();
    },

    /** Cycles through the loaded data and base layer input arrays and makes
     *   the necessary calls to the Map object such that that the map's
     *   visual state corresponds to what the user has selected in the control
     *
     * @private
     */
    updateMap: function() {

        // set the newly selected base layer
        for(var i=0; i < this.baseLayers.length; i++) {
            var layerEntry = this.baseLayers[i];
            if (layerEntry.inputElem.checked) {
                this.map.setBaseLayer(layerEntry.layer, false);
            }
        }

        // set the correct visibilities for the overlays
        for(var i=0; i < this.dataLayers.length; i++) {
            var layerEntry = this.dataLayers[i];
            layerEntry.layer.setVisibility(layerEntry.inputElem.checked, true);
        }

    },

    /** Set up the labels and divs for the control
     *
     * @param {Event} e
     */
    maximizeControl: function(e) {

        //HACK HACK HACK - find a way to auto-size this layerswitcher
        this.div.style.width = "20em";
        this.div.style.height = "";

        this.showControls(false);

        if (e != null) {
            OpenLayers.Event.stop(e);
        }
    },

    /** Hide all the contents of the control, shrink the size,
     *   add the maximize icon
     *
     * @param {Event} e
     */
    minimizeControl: function(e) {

        this.div.style.width = "0px";
        this.div.style.height = "0px";

        this.showControls(true);

        if (e != null) {
            OpenLayers.Event.stop(e);
        }
    },

    /** Hide/Show all LayerSwitcher controls depending on whether we are
     *   minimized or not
     *
     * @private
     *
     * @param {Boolean} minimize
     */
    showControls: function(minimize) {

        this.maximizeDiv.style.display = minimize ? "" : "none";
        this.minimizeDiv.style.display = minimize ? "none" : "";

        this.layersDiv.style.display = minimize ? "none" : "";
    },

    /** Set up the labels and divs for the control
     *
     */
    loadContents: function() {

        //configure main div
        this.div.style.position = "absolute";
        this.div.style.top = "25px";
        this.div.style.right = "0px";
        this.div.style.left = "";
        this.div.style.fontFamily = "sans-serif";
        this.div.style.fontWeight = "bold";
        this.div.style.marginTop = "3px";
        this.div.style.marginLeft = "3px";
        this.div.style.marginBottom = "3px";
        this.div.style.fontSize = "smaller";
        this.div.style.color = "white";
        this.div.style.backgroundColor = "transparent";

        OpenLayers.Event.observe(this.div, "mouseup",
                      this.mouseUp.bindAsEventListener(this));
        OpenLayers.Event.observe(this.div, "click",
                      this.ignoreEvent);
        OpenLayers.Event.observe(this.div, "mousedown",
                      this.mouseDown.bindAsEventListener(this));
        OpenLayers.Event.observe(this.div, "dblclick", this.ignoreEvent);

        // layers list div
        this.layersDiv = document.createElement("div");
        this.layersDiv.id = "layersDiv";
        this.layersDiv.style.paddingTop = "5px";
        this.layersDiv.style.paddingLeft = "10px";
        this.layersDiv.style.paddingBottom = "5px";
        this.layersDiv.style.paddingRight = "75px";
        this.layersDiv.style.backgroundColor = this.activeColor;

        // had to set width/height to get transparency in IE to work.
        // thanks -- http://jszen.blogspot.com/2005/04/ie6-opacity-filter-caveat.html
        //
        this.layersDiv.style.width = "100%";
        this.layersDiv.style.height = "100%";

        var baseLbl = document.createElement("div");
        baseLbl.innerHTML = "Base Layer";
        baseLbl.style.marginTop = "3px";
        baseLbl.style.marginLeft = "3px";
        baseLbl.style.marginBottom = "3px";

        this.baseLayersDiv = document.createElement("div");
        this.baseLayersDiv.style.paddingLeft = "10px";
        /*OpenLayers.Event.observe(this.baseLayersDiv, "click",
                      this.onLayerClick.bindAsEventListener(this));
        */

        this.dataLbl = document.createElement("div");
        this.dataLbl.innerHTML = "Overlays";
        this.dataLbl.style.marginTop = "3px";
        this.dataLbl.style.marginLeft = "3px";
        this.dataLbl.style.marginBottom = "3px";

        this.dataLayersDiv = document.createElement("div");
        this.dataLayersDiv.style.paddingLeft = "10px";

        if (this.ascending) {
            this.layersDiv.appendChild(baseLbl);
            this.layersDiv.appendChild(this.baseLayersDiv);
            this.layersDiv.appendChild(this.dataLbl);
            this.layersDiv.appendChild(this.dataLayersDiv);
        } else {
            this.layersDiv.appendChild(this.dataLbl);
            this.layersDiv.appendChild(this.dataLayersDiv);
            this.layersDiv.appendChild(baseLbl);
            this.layersDiv.appendChild(this.baseLayersDiv);
        }

        this.div.appendChild(this.layersDiv);

        OpenLayers.Rico.Corner.round(this.div, {corners: "tl bl",
                                        bgColor: "transparent",
                                        color: this.activeColor,
                                        blend: false});

        OpenLayers.Rico.Corner.changeOpacity(this.layersDiv, 0.75);

        var imgLocation = OpenLayers.Util.getImagesLocation();
        var sz = new OpenLayers.Size(18,18);

        // maximize button div
        var img = imgLocation + 'layer-switcher-maximize.png';
        this.maximizeDiv = OpenLayers.Util.createAlphaImageDiv(
                                    "OpenLayers_Control_MaximizeDiv",
                                    null,
                                    sz,
                                    img,
                                    "absolute");
        this.maximizeDiv.style.top = "5px";
        this.maximizeDiv.style.right = "0px";
        this.maximizeDiv.style.left = "";
        this.maximizeDiv.style.display = "none";
        OpenLayers.Event.observe(this.maximizeDiv,
                      "click",
                      this.maximizeControl.bindAsEventListener(this));

        this.div.appendChild(this.maximizeDiv);

        // minimize button div
        var img = imgLocation + 'layer-switcher-minimize.png';
        var sz = new OpenLayers.Size(18,18);
        this.minimizeDiv = OpenLayers.Util.createAlphaImageDiv(
                                    "OpenLayers_Control_MinimizeDiv",
                                    null,
                                    sz,
                                    img,
                                    "absolute");
        this.minimizeDiv.style.top = "5px";
        this.minimizeDiv.style.right = "0px";
        this.minimizeDiv.style.left = "";
        this.minimizeDiv.style.display = "none";
        OpenLayers.Event.observe(this.minimizeDiv,
                      "click",
                      this.minimizeControl.bindAsEventListener(this));

        this.div.appendChild(this.minimizeDiv);
    },

    /**
     * @private
     *
     * @param {Event} evt
     */
    ignoreEvent: function(evt) {
        OpenLayers.Event.stop(evt);
    },

    /** Register a local 'mouseDown' flag so that we'll know whether or not
     *   to ignore a mouseUp event
     *
     * @private
     *
     * @param {Event} evt
     */
    mouseDown: function(evt) {
        this.mouseDown = true;
        this.ignoreEvent(evt);
    },

    /** If the 'mouseDown' flag has been set, that means that the drag was
     *   started from within the LayerSwitcher control, and thus we can
     *   ignore the mouseup. Otherwise, let the Event continue.
     *
     * @private
     *
     * @param {Event} evt
     */
    mouseUp: function(evt) {
        if (this.mouseDown) {
            this.mouseDown = false;
            this.ignoreEvent(evt);
        }
    },

    /** @final @type String */
    CLASS_NAME: "OpenLayers.Control.LayerSwitcher"
});
<|endoftext|>


<commit_before>// @flow
import React from 'react'
import {Box, Icon, Text, PopupDialog} from '../../../common-adapters/index'
import {AttachmentPopupMenu} from '../messages/popup'
import {globalColors, globalMargins, globalStyles} from '../../../styles'
import {fileUIName} from '../../../constants/platform'

import type {Props} from './'
import type {Message} from '../../../constants/chat'

const AttachmentStatusFooter = ({message, onDownloadAttachment, onOpenInFileUI}: {message: Message, onDownloadAttachment: () => void, onOpenInFileUI: () => void}) => {
  let actionHandler
  let actionText
  if (message.downloadedPath) {
    actionHandler = onOpenInFileUI
    actionText = `Show in ${fileUIName}`
  } else {
    actionHandler = onDownloadAttachment
    actionText = 'Download'
  }

  return (
    <Box style={styleHeaderFooter}>
      <Text type='BodySmall' style={{color: globalColors.black_60, cursor: 'pointer'}} onClick={actionHandler}>{actionText}</Text>
    </Box>
  )
}

const AttachmentPopup = ({message, detailsPopupShowing, isZoomed, onCloseDetailsPopup, onClose, onDownloadAttachment, onDeleteMessage, onOpenDetailsPopup, onToggleZoom, onOpenInFileUI, you}: Props) => (
  <PopupDialog onClose={onClose} fill={true}>
    {detailsPopupShowing && <AttachmentPopupMenu
      you={you}
      message={message}
      onDeleteMessage={onDeleteMessage}
      onDownloadAttachment={onDownloadAttachment}
      onOpenInFileUI={onOpenInFileUI}
      onHidden={onCloseDetailsPopup}
      style={{position: 'absolute', top: 28, right: globalMargins.xtiny}}
    />}
    <Box style={styleHeaderFooter}>
      <Text type='BodySemibold' style={{color: globalColors.black_75, flex: 1}}>{message.title}</Text>
      <Icon type='iconfont-ellipsis' style={{color: globalColors.black_40, cursor: 'pointer'}} onClick={detailsPopupShowing ? onCloseDetailsPopup : onOpenDetailsPopup} />
    </Box>
    <Box style={isZoomed ? styleContentsZoom : styleContentsFit} onClick={onToggleZoom}>

    </Box>
    <AttachmentStatusFooter message={message} onDownloadAttachment={onDownloadAttachment} onOpenInFileUI={onOpenInFileUI} />
  </PopupDialog>
)

const styleHeaderFooter = {
  ...globalStyles.flexBoxRow,
  height: 32,
  alignItems: 'center',
  marginLeft: globalMargins.tiny,
  marginRight: globalMargins.tiny,
  flexShrink: 0,
}

const styleContentsFit = {
  ...globalStyles.flexBoxRow,
  flex: 1,
}

const styleContentsZoom = {
  display: 'block',
  overflow: 'auto',
  flex: 1,
}

const styleImageFit = {
  display: 'block',
  cursor: 'zoom-in',
  objectFit: 'scale-down',
  width: '100%',
}

const styleImageZoom = {
  display: 'block',
  cursor: 'zoom-out',
  minWidth: '100%',
  minHeight: '100%',
}

export default AttachmentPopup
<commit_msg>Display spinner in attachment popup when loading<commit_after>// @flow
import React from 'react'
import {Box, Icon, Text, PopupDialog, ProgressIndicator} from '../../../common-adapters/index'
import {AttachmentPopupMenu} from '../messages/popup'
import {globalColors, globalMargins, globalStyles} from '../../../styles'
import {fileUIName} from '../../../constants/platform'

import type {Props} from './'
import type {Message} from '../../../constants/chat'

const AttachmentStatusFooter = ({message, onDownloadAttachment, onOpenInFileUI}: {message: Message, onDownloadAttachment: () => void, onOpenInFileUI: () => void}) => {
  let actionHandler
  let actionText
  if (message.downloadedPath) {
    actionHandler = onOpenInFileUI
    actionText = `Show in ${fileUIName}`
  } else {
    actionHandler = onDownloadAttachment
    actionText = 'Download'
  }

  return (
    <Box style={styleHeaderFooter}>
      <Text type='BodySmall' style={{color: globalColors.black_60, cursor: 'pointer'}} onClick={actionHandler}>{actionText}</Text>
    </Box>
  )
}

const AttachmentView = ({isZoomed, onToggleZoom, path}: {isZoomed: boolean, onToggleZoom: () => void, path: ?string}) => {
  if (path) {
    return (
      <Box style={isZoomed ? styleContentsZoom : styleContentsFit} onClick={onToggleZoom}>

      </Box>
    )
  } else {
    return (
      <Box style={styleContentsCenter}>
        <ProgressIndicator style={{width: 48}} />
      </Box>
    )
  }
}

const AttachmentPopup = ({message, detailsPopupShowing, isZoomed, onCloseDetailsPopup, onClose, onDownloadAttachment, onDeleteMessage, onOpenDetailsPopup, onToggleZoom, onOpenInFileUI, you}: Props) => (
  <PopupDialog onClose={onClose} fill={true}>
    {detailsPopupShowing && <AttachmentPopupMenu
      you={you}
      message={message}
      onDeleteMessage={onDeleteMessage}
      onDownloadAttachment={onDownloadAttachment}
      onOpenInFileUI={onOpenInFileUI}
      onHidden={onCloseDetailsPopup}
      style={{position: 'absolute', top: 28, right: globalMargins.xtiny}}
    />}
    <Box style={styleHeaderFooter}>
      <Text type='BodySemibold' style={{color: globalColors.black_75, flex: 1}}>{message.title}</Text>
      <Icon type='iconfont-ellipsis' style={{color: globalColors.black_40, cursor: 'pointer'}} onClick={detailsPopupShowing ? onCloseDetailsPopup : onOpenDetailsPopup} />
    </Box>
    <AttachmentView isZoomed={isZoomed} onToggleZoom={onToggleZoom} path={message.downloadedPath} />
    <AttachmentStatusFooter message={message} onDownloadAttachment={onDownloadAttachment} onOpenInFileUI={onOpenInFileUI} />
  </PopupDialog>
)

const styleHeaderFooter = {
  ...globalStyles.flexBoxRow,
  height: 32,
  alignItems: 'center',
  marginLeft: globalMargins.tiny,
  marginRight: globalMargins.tiny,
  flexShrink: 0,
}

const styleContentsFit = {
  ...globalStyles.flexBoxRow,
  flex: 1,
}

const styleContentsCenter = {
  ...styleContentsFit,
  alignItems: 'center',
  justifyContent: 'center',
}

const styleContentsZoom = {
  display: 'block',
  overflow: 'auto',
  flex: 1,
}

const styleImageFit = {
  display: 'block',
  cursor: 'zoom-in',
  objectFit: 'scale-down',
  width: '100%',
}

const styleImageZoom = {
  display: 'block',
  cursor: 'zoom-out',
  minWidth: '100%',
  minHeight: '100%',
}

export default AttachmentPopup
<|endoftext|>


<commit_before>var phantom = require('phantom')
  , _ = require('lodash')
  , util = require('./util.js')
  , zlib = require('zlib');

var PAGE_DONE_CHECK_TIMEOUT = process.env.PAGE_DONE_CHECK_TIMEOUT || 50;

var RESOURCE_DOWNLOAD_TIMEOUT = process.env.RESOURCE_DOWNLOAD_TIMEOUT || 10 * 1000;

var WAIT_AFTER_LAST_REQUEST = process.env.WAIT_AFTER_LAST_REQUEST || 500;

var JS_CHECK_TIMEOUT = process.env.JS_CHECK_TIMEOUT || 50;

var JS_TIMEOUT = process.env.JS_TIMEOUT || 10 * 1000;

var EVALUATE_JAVASCRIPT_CHECK_TIMEOUT = process.env.EVALUATE_JAVASCRIPT_CHECK_TIMEOUT || 50;

var server = exports = module.exports = {};

server.init = function(options) {
    this.plugins = this.plugins || [];
    this.options = options;

    return this;
};

server.start = function() {
    if(!this.options.isMaster) {
        this.createPhantom();
    }
};

server.use = function(plugin) {
    this.plugins.push(plugin);
    if (typeof plugin.init === 'function') plugin.init(this);
};

server._pluginEvent = function(methodName, args, callback) {
    var _this = this
      , index = 0
      , next;

    next = function() {
        var layer = _this.plugins[index++];
        if (!layer) return callback();

        var method = layer[methodName];

        if (method) {
            method.apply(layer, args);
        } else {
            next();
        }
    };

    args.push(next);
    next();
};

server.createPhantom = function() {
    var _this = this;
    console.log('starting phantom');

    var args = ["--load-images=false", "--ignore-ssl-errors=true", "--ssl-protocol=tlsv1"];
    var port = this.options.phantomBasePort || 12300;

    if(this.options.phantomArguments) {
        args = this.options.phantomArguments;
    }

    args.push({
        port: port + this.options.worker.id,
        binary: require('phantomjs').path,
        onExit: function() {
            _this.phantom = null;
            console.log('phantom crashed, restarting...');
            process.nextTick(_.bind(_this.createPhantom, _this));
        }
    });

    args.push(_.bind(this.onPhantomCreate, this));

    phantom.create.apply(this, args);
};

server.onPhantomCreate = function(phantom) {
    console.log('started phantom');
    this.phantom = phantom;
    this.phantom.id = Math.random().toString(36);
};

server.onRequest = function(req, res) {
    var _this = this;

    // Create a partial out of the _send method for the convenience of plugins
    res.send = _.bind(this._send, this, req, res);

    req.prerender = {
        url: util.getUrl(req),
        start: new Date()
    };

    console.log('getting', req.prerender.url);

    this._pluginEvent("beforePhantomRequest", [req, res], function() {
        _this.createPage(req, res);
    });
};

server.createPage = function(req, res) {
    var _this = this;

    if(!this.phantom) {
        setTimeout(function(){
            _this.createPage(req, res);
        }, 50);
    } else {
        req.prerender.phantomId = this.phantom.id;
        this.phantom.createPage(function(page){
            req.prerender.page = page;
            _this.onPhantomPageCreate(req, res);
        });
    }
};

server.onPhantomPageCreate = function(req, res) {
    var _this = this;

    req.prerender.stage = 0;
    req.prerender.pendingRequests = 1;

    // Listen for updates on resource downloads
    req.prerender.page.onResourceRequested(this.onResourceRequested, _.bind(_this.onResourceRequestedCallback, _this, req, res));
    req.prerender.page.set('onResourceReceived', _.bind(_this.onResourceReceived, _this, req, res));
    req.prerender.page.set('onResourceError', _.bind(_this.onResourceError, _this, req, res));
    req.prerender.page.set('onResourceTimeout', _.bind(_this.onResourceTimeout, _this, req, res));

    req.prerender.page.set('viewportSize', { width: 1440, height: 718 });

    req.prerender.page.get('settings.userAgent', function(userAgent) {
        req.prerender.page.set('settings.userAgent', userAgent + ' Prerender (+https://github.com/prerender/prerender)');

        // Fire off a middleware event, then download all of the assets
        _this._pluginEvent("onPhantomPageCreate", [_this.phantom, req, res], function() {
            req.prerender.downloadStarted = req.prerender.lastResourceReceived = new Date();

            req.prerender.downloadChecker = setInterval(function() {
                _this.checkIfPageIsDoneLoading(req, res, req.prerender.status === 'fail');
            }, _this.options.pageDoneCheckTimeout || PAGE_DONE_CHECK_TIMEOUT);

            req.prerender.page.open(encodeURI(req.prerender.url.replace(/%20/g, ' ')), function(status) {
                req.prerender.status = status;
            });
        });
    });
};

//We want to abort the request if it's a call to Google Analytics or other tracking services.
server.onResourceRequested = function (requestData, request) {
    if ((/google-analytics.com/gi).test(requestData.url) ||
        (/api.mixpanel.com/gi).test(requestData.url) ||
        (/fonts.googleapis.com/gi).test(requestData.url) ||
        (/stats.g.doubleclick.net/gi).test(requestData.url) ||
        (/mc.yandex.ru/gi).test(requestData.url)){
        request.abort();
    }
};

// Increment the number of pending requests left to download when a new
// resource is requested
server.onResourceRequestedCallback = function (req) {
    req.prerender.pendingRequests++;
};

// Decrement the number of pending requests left to download after a resource
// is downloaded
server.onResourceReceived = function (req, res, response) {
    req.prerender.lastResourceReceived = new Date();

    //sometimes on redirects, phantomjs doesnt fire the 'end' stage of the original request, so we need to check it here
    if(util.normalizeUrl(req.prerender.url) === util.normalizeUrl(response.url) && response.status >= 300 && response.status <= 399) {

        if (response.redirectURL) {
            req.prerender.redirectURL = response.redirectURL;
        } else {
            var match = _.findWhere(response.headers, { name: 'Location' });
            if (match) {
                req.prerender.redirectURL = util.normalizeUrl(match.value);
            }
        }

        req.prerender.statusCode = response.status;

        if(!(this.options.followRedirect || process.env.FOLLOW_REDIRECT)) {
            //force the response now
            return this.checkIfPageIsDoneLoading(req, res, true);
        }
    }

    if ('end' === response.stage) {
        req.prerender.pendingRequests--;

        if (util.normalizeUrl(req.prerender.url) === util.normalizeUrl(response.url)) {
            req.prerender.pendingRequests--;

            req.prerender.statusCode = response.status;
        }

        if( (this.options.followRedirect || process.env.FOLLOW_REDIRECT) && req.prerender.redirectURL && util.normalizeUrl(req.prerender.redirectURL) === util.normalizeUrl(response.url)) {
            req.prerender.statusCode = response.status;
        }
    }
};

// Decrement the number of pending requests to download when there's an error
// fetching a resource
server.onResourceError = function(req) {
    req.prerender.pendingRequests--;
};

// Decrement the number of pending requests to download when there's a timeout
// fetching a resource
server.onResourceTimeout = function(req) {
    req.prerender.pendingRequests--;
};

// Called occasionally to check if a page is completely loaded
server.checkIfPageIsDoneLoading = function(req, res, force) {
    var timedOut = new Date().getTime() - req.prerender.downloadStarted.getTime() > (this.options.resourceDownloadTimeout || RESOURCE_DOWNLOAD_TIMEOUT)
      , timeSinceLastRequest = new Date().getTime() - req.prerender.lastResourceReceived.getTime();

    // Check against the current stage to make sure we don't finish more than
    // once, and check against a bunch of states that would signal finish - if
    // resource downloads have timed out, if the page has errored out, or if
    // there are no pending requests left
    if(req.prerender.stage < 1 && (force || (req.prerender.pendingRequests <= 0 && timeSinceLastRequest > (this.options.waitAfterLastRequest || WAIT_AFTER_LAST_REQUEST)) || timedOut)) {
        req.prerender.stage = 1;
        clearInterval(req.prerender.downloadChecker);
        req.prerender.downloadChecker = null;

        if(req.prerender.statusCode && req.prerender.statusCode >= 300 && req.prerender.statusCode <= 399) {
            // Finish up if we got a redirect status code
            res.send(req.prerender.statusCode);
        } else {
            // Now evaluate the javascript
            req.prerender.timeoutChecker = setInterval(_.bind(this.checkIfJavascriptTimedOut, this, req, res), (this.options.jsCheckTimeout || JS_CHECK_TIMEOUT));
            this.evaluateJavascriptOnPage(req, res);
        }
    }
};

// Checks to see if the execution of javascript has timed out
server.checkIfJavascriptTimedOut = function(req, res) {

    var timeout = new Date().getTime() - req.prerender.downloadStarted.getTime() > (this.options.jsTimeout || JS_TIMEOUT);

    if (timeout && req.prerender.lastJavascriptExecution && new Date().getTime() - req.prerender.lastJavascriptExecution.getTime() < 2000) {
        console.log('Timed out. Sending request with HTML on the page');
        clearInterval(req.prerender.timeoutChecker);
        req.prerender.timeoutChecker = null;

        this.onPageEvaluate(req, res);
    } else if (timeout && req.prerender.stage < 2) {
        console.log('Experiencing infinite javascript loop. Killing phantomjs...');
        clearInterval(req.prerender.timeoutChecker);
        req.prerender.timeoutChecker = null;

        res.send(504, {abort: true});
    }
};

// Evaluates the javascript on the page
server.evaluateJavascriptOnPage = function(req, res) {
    var _this = this;

    if(req.prerender.stage >= 2) return;

    req.prerender.page.evaluate(this.javascriptToExecuteOnPage, function(obj) {
        // Update the evaluated HTML
        req.prerender.documentHTML = obj.html;
        req.prerender.lastJavascriptExecution = new Date();

        if(!obj.shouldWaitForPrerenderReady || (obj.shouldWaitForPrerenderReady && obj.prerenderReady)) {
            clearInterval(req.prerender.timeoutChecker);
            req.prerender.timeoutChecker = null;

            _this.onPageEvaluate(req, res);
        } else {
            setTimeout(_.bind(_this.evaluateJavascriptOnPage, _this, req, res), (this.evaluateJavascriptCheckTimout || EVALUATE_JAVASCRIPT_CHECK_TIMEOUT));
        }
    });
};

// Fetches the html on the page
server.javascriptToExecuteOnPage = function() {
    try {
        var doctype = ''
          , html = document && document.getElementsByTagName('html');

        if(document.doctype) {
            doctype = "<!DOCTYPE "
                 + document.doctype.name
                 + (document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '')
                 + (!document.doctype.publicId && document.doctype.systemId ? ' SYSTEM' : '')
                 + (document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '')
                 + '>';
        }

        if (html && html[0]) {
            return {
                html: doctype + html[0].outerHTML,
                shouldWaitForPrerenderReady: typeof window.prerenderReady === 'boolean',
                prerenderReady: window.prerenderReady
            };
        }

    } catch (e) { }

    return  {
        html: '',
        shouldWaitForPrerenderReady: false,
        prerenderReady: window.prerenderReady
    };
};

// Called when we're done evaluating the javascript on the page
server.onPageEvaluate = function(req, res) {

    if(req.prerender.stage >= 2) return;

    req.prerender.stage = 2;

    if (!req.prerender.documentHTML) {
        res.send(req.prerender.statusCode || 404);
    } else {
        this._pluginEvent("afterPhantomRequest", [req, res], function() {
            res.send(req.prerender.statusCode || 200);
        });
    }
};

server._send = function(req, res, statusCode, options) {
    var _this = this;

    if(req.prerender.page) {
        req.prerender.page.close();
        req.prerender.page = null;
    }
    req.prerender.stage = 2;

    req.prerender.documentHTML = options || req.prerender.documentHTML;
    req.prerender.statusCode = statusCode || req.prerender.statusCode;

    if(req.prerender.statusCode) {
        req.prerender.statusCode = parseInt(req.prerender.statusCode);
    }

    if (options && typeof options === 'object' && !Buffer.isBuffer(options)) {
        req.prerender.documentHTML = options.documentHTML;
        req.prerender.redirectURL = options.redirectURL;
    }

    this._pluginEvent("beforeSend", [req, res], function() {
        if (req.prerender.redirectURL) {
            res.setHeader('Location', req.prerender.redirectURL);
        }

        res.setHeader('Content-Type', 'text/html;charset=UTF-8');

        if(req.headers['accept-encoding'] && req.headers['accept-encoding'].indexOf('gzip') >= 0) {
            res.setHeader('Content-Encoding', 'gzip');
            zlib.gzip(req.prerender.documentHTML, function(err, result) {
                req.prerender.documentHTML = result;
                _this._sendResponse.apply(_this, [req, res, options]);
            });
        } else {
            _this._sendResponse.apply(_this, [req, res, options]);
        }
    });
};

server._sendResponse = function(req, res, options) {

    if (req.prerender.documentHTML) {
        if(Buffer.isBuffer(req.prerender.documentHTML)) {
            res.setHeader('Content-Length', req.prerender.documentHTML.length);
        } else {
            res.setHeader('Content-Length', Buffer.byteLength(req.prerender.documentHTML, 'utf8'));
        }
    }

    res.writeHead(req.prerender.statusCode || 504);

    if (req.prerender.documentHTML) res.write(req.prerender.documentHTML);

    res.end();

    var ms = new Date().getTime() - req.prerender.start.getTime();
    console.log('got', req.prerender.statusCode, 'in', ms + 'ms', 'for', req.prerender.url);

    if(options && options.abort) {
        this.options.worker.kill();
       //  try {
       //     //not happy with this... but when phantomjs is hanging, it can't exit any normal way
       //     console.log('Experiencing infinite javascript loop. Killing phantomjs...');
       //     require('child_process').spawn('pkill', ['phantomjs']);
       //     _this.phantom = null;
       // } catch(e) {
       //     console.log('Error killing phantomjs from javascript infinite loop:', e);
       // }
    }
};
<commit_msg>Do not add Location header when following redirect<commit_after>var phantom = require('phantom')
  , _ = require('lodash')
  , util = require('./util.js')
  , zlib = require('zlib');

var PAGE_DONE_CHECK_TIMEOUT = process.env.PAGE_DONE_CHECK_TIMEOUT || 50;

var RESOURCE_DOWNLOAD_TIMEOUT = process.env.RESOURCE_DOWNLOAD_TIMEOUT || 10 * 1000;

var WAIT_AFTER_LAST_REQUEST = process.env.WAIT_AFTER_LAST_REQUEST || 500;

var JS_CHECK_TIMEOUT = process.env.JS_CHECK_TIMEOUT || 50;

var JS_TIMEOUT = process.env.JS_TIMEOUT || 10 * 1000;

var EVALUATE_JAVASCRIPT_CHECK_TIMEOUT = process.env.EVALUATE_JAVASCRIPT_CHECK_TIMEOUT || 50;

var server = exports = module.exports = {};

server.init = function(options) {
    this.plugins = this.plugins || [];
    this.options = options;

    return this;
};

server.start = function() {
    if(!this.options.isMaster) {
        this.createPhantom();
    }
};

server.use = function(plugin) {
    this.plugins.push(plugin);
    if (typeof plugin.init === 'function') plugin.init(this);
};

server._pluginEvent = function(methodName, args, callback) {
    var _this = this
      , index = 0
      , next;

    next = function() {
        var layer = _this.plugins[index++];
        if (!layer) return callback();

        var method = layer[methodName];

        if (method) {
            method.apply(layer, args);
        } else {
            next();
        }
    };

    args.push(next);
    next();
};

server.createPhantom = function() {
    var _this = this;
    console.log('starting phantom');

    var args = ["--load-images=false", "--ignore-ssl-errors=true", "--ssl-protocol=tlsv1"];
    var port = this.options.phantomBasePort || 12300;

    if(this.options.phantomArguments) {
        args = this.options.phantomArguments;
    }

    args.push({
        port: port + this.options.worker.id,
        binary: require('phantomjs').path,
        onExit: function() {
            _this.phantom = null;
            console.log('phantom crashed, restarting...');
            process.nextTick(_.bind(_this.createPhantom, _this));
        }
    });

    args.push(_.bind(this.onPhantomCreate, this));

    phantom.create.apply(this, args);
};

server.onPhantomCreate = function(phantom) {
    console.log('started phantom');
    this.phantom = phantom;
    this.phantom.id = Math.random().toString(36);
};

server.onRequest = function(req, res) {
    var _this = this;

    // Create a partial out of the _send method for the convenience of plugins
    res.send = _.bind(this._send, this, req, res);

    req.prerender = {
        url: util.getUrl(req),
        start: new Date()
    };

    console.log('getting', req.prerender.url);

    this._pluginEvent("beforePhantomRequest", [req, res], function() {
        _this.createPage(req, res);
    });
};

server.createPage = function(req, res) {
    var _this = this;

    if(!this.phantom) {
        setTimeout(function(){
            _this.createPage(req, res);
        }, 50);
    } else {
        req.prerender.phantomId = this.phantom.id;
        this.phantom.createPage(function(page){
            req.prerender.page = page;
            _this.onPhantomPageCreate(req, res);
        });
    }
};

server.onPhantomPageCreate = function(req, res) {
    var _this = this;

    req.prerender.stage = 0;
    req.prerender.pendingRequests = 1;

    // Listen for updates on resource downloads
    req.prerender.page.onResourceRequested(this.onResourceRequested, _.bind(_this.onResourceRequestedCallback, _this, req, res));
    req.prerender.page.set('onResourceReceived', _.bind(_this.onResourceReceived, _this, req, res));
    req.prerender.page.set('onResourceError', _.bind(_this.onResourceError, _this, req, res));
    req.prerender.page.set('onResourceTimeout', _.bind(_this.onResourceTimeout, _this, req, res));

    req.prerender.page.set('viewportSize', { width: 1440, height: 718 });

    req.prerender.page.get('settings.userAgent', function(userAgent) {
        req.prerender.page.set('settings.userAgent', userAgent + ' Prerender (+https://github.com/prerender/prerender)');

        // Fire off a middleware event, then download all of the assets
        _this._pluginEvent("onPhantomPageCreate", [_this.phantom, req, res], function() {
            req.prerender.downloadStarted = req.prerender.lastResourceReceived = new Date();

            req.prerender.downloadChecker = setInterval(function() {
                _this.checkIfPageIsDoneLoading(req, res, req.prerender.status === 'fail');
            }, _this.options.pageDoneCheckTimeout || PAGE_DONE_CHECK_TIMEOUT);

            req.prerender.page.open(encodeURI(req.prerender.url.replace(/%20/g, ' ')), function(status) {
                req.prerender.status = status;
            });
        });
    });
};

//We want to abort the request if it's a call to Google Analytics or other tracking services.
server.onResourceRequested = function (requestData, request) {
    if ((/google-analytics.com/gi).test(requestData.url) ||
        (/api.mixpanel.com/gi).test(requestData.url) ||
        (/fonts.googleapis.com/gi).test(requestData.url) ||
        (/stats.g.doubleclick.net/gi).test(requestData.url) ||
        (/mc.yandex.ru/gi).test(requestData.url)){
        request.abort();
    }
};

// Increment the number of pending requests left to download when a new
// resource is requested
server.onResourceRequestedCallback = function (req) {
    req.prerender.pendingRequests++;
};

// Decrement the number of pending requests left to download after a resource
// is downloaded
server.onResourceReceived = function (req, res, response) {
    req.prerender.lastResourceReceived = new Date();

    //sometimes on redirects, phantomjs doesnt fire the 'end' stage of the original request, so we need to check it here
    if(util.normalizeUrl(req.prerender.url) === util.normalizeUrl(response.url) && response.status >= 300 && response.status <= 399) {

        if (response.redirectURL) {
            req.prerender.redirectURL = response.redirectURL;
        } else {
            var match = _.findWhere(response.headers, { name: 'Location' });
            if (match) {
                req.prerender.redirectURL = util.normalizeUrl(match.value);
            }
        }

        req.prerender.statusCode = response.status;

        if(!(this.options.followRedirect || process.env.FOLLOW_REDIRECT)) {
            //force the response now
            return this.checkIfPageIsDoneLoading(req, res, true);
        }
    }

    if ('end' === response.stage) {
        req.prerender.pendingRequests--;

        if (util.normalizeUrl(req.prerender.url) === util.normalizeUrl(response.url)) {
            req.prerender.pendingRequests--;

            req.prerender.statusCode = response.status;
        }

        if( (this.options.followRedirect || process.env.FOLLOW_REDIRECT) && req.prerender.redirectURL && util.normalizeUrl(req.prerender.redirectURL) === util.normalizeUrl(response.url)) {
            req.prerender.statusCode = response.status;
        }
    }
};

// Decrement the number of pending requests to download when there's an error
// fetching a resource
server.onResourceError = function(req) {
    req.prerender.pendingRequests--;
};

// Decrement the number of pending requests to download when there's a timeout
// fetching a resource
server.onResourceTimeout = function(req) {
    req.prerender.pendingRequests--;
};

// Called occasionally to check if a page is completely loaded
server.checkIfPageIsDoneLoading = function(req, res, force) {
    var timedOut = new Date().getTime() - req.prerender.downloadStarted.getTime() > (this.options.resourceDownloadTimeout || RESOURCE_DOWNLOAD_TIMEOUT)
      , timeSinceLastRequest = new Date().getTime() - req.prerender.lastResourceReceived.getTime();

    // Check against the current stage to make sure we don't finish more than
    // once, and check against a bunch of states that would signal finish - if
    // resource downloads have timed out, if the page has errored out, or if
    // there are no pending requests left
    if(req.prerender.stage < 1 && (force || (req.prerender.pendingRequests <= 0 && timeSinceLastRequest > (this.options.waitAfterLastRequest || WAIT_AFTER_LAST_REQUEST)) || timedOut)) {
        req.prerender.stage = 1;
        clearInterval(req.prerender.downloadChecker);
        req.prerender.downloadChecker = null;

        if(req.prerender.statusCode && req.prerender.statusCode >= 300 && req.prerender.statusCode <= 399) {
            // Finish up if we got a redirect status code
            res.send(req.prerender.statusCode);
        } else {
            // Now evaluate the javascript
            req.prerender.timeoutChecker = setInterval(_.bind(this.checkIfJavascriptTimedOut, this, req, res), (this.options.jsCheckTimeout || JS_CHECK_TIMEOUT));
            this.evaluateJavascriptOnPage(req, res);
        }
    }
};

// Checks to see if the execution of javascript has timed out
server.checkIfJavascriptTimedOut = function(req, res) {

    var timeout = new Date().getTime() - req.prerender.downloadStarted.getTime() > (this.options.jsTimeout || JS_TIMEOUT);

    if (timeout && req.prerender.lastJavascriptExecution && new Date().getTime() - req.prerender.lastJavascriptExecution.getTime() < 2000) {
        console.log('Timed out. Sending request with HTML on the page');
        clearInterval(req.prerender.timeoutChecker);
        req.prerender.timeoutChecker = null;

        this.onPageEvaluate(req, res);
    } else if (timeout && req.prerender.stage < 2) {
        console.log('Experiencing infinite javascript loop. Killing phantomjs...');
        clearInterval(req.prerender.timeoutChecker);
        req.prerender.timeoutChecker = null;

        res.send(504, {abort: true});
    }
};

// Evaluates the javascript on the page
server.evaluateJavascriptOnPage = function(req, res) {
    var _this = this;

    if(req.prerender.stage >= 2) return;

    req.prerender.page.evaluate(this.javascriptToExecuteOnPage, function(obj) {
        // Update the evaluated HTML
        req.prerender.documentHTML = obj.html;
        req.prerender.lastJavascriptExecution = new Date();

        if(!obj.shouldWaitForPrerenderReady || (obj.shouldWaitForPrerenderReady && obj.prerenderReady)) {
            clearInterval(req.prerender.timeoutChecker);
            req.prerender.timeoutChecker = null;

            _this.onPageEvaluate(req, res);
        } else {
            setTimeout(_.bind(_this.evaluateJavascriptOnPage, _this, req, res), (this.evaluateJavascriptCheckTimout || EVALUATE_JAVASCRIPT_CHECK_TIMEOUT));
        }
    });
};

// Fetches the html on the page
server.javascriptToExecuteOnPage = function() {
    try {
        var doctype = ''
          , html = document && document.getElementsByTagName('html');

        if(document.doctype) {
            doctype = "<!DOCTYPE "
                 + document.doctype.name
                 + (document.doctype.publicId ? ' PUBLIC "' + document.doctype.publicId + '"' : '')
                 + (!document.doctype.publicId && document.doctype.systemId ? ' SYSTEM' : '')
                 + (document.doctype.systemId ? ' "' + document.doctype.systemId + '"' : '')
                 + '>';
        }

        if (html && html[0]) {
            return {
                html: doctype + html[0].outerHTML,
                shouldWaitForPrerenderReady: typeof window.prerenderReady === 'boolean',
                prerenderReady: window.prerenderReady
            };
        }

    } catch (e) { }

    return  {
        html: '',
        shouldWaitForPrerenderReady: false,
        prerenderReady: window.prerenderReady
    };
};

// Called when we're done evaluating the javascript on the page
server.onPageEvaluate = function(req, res) {

    if(req.prerender.stage >= 2) return;

    req.prerender.stage = 2;

    if (!req.prerender.documentHTML) {
        res.send(req.prerender.statusCode || 404);
    } else {
        this._pluginEvent("afterPhantomRequest", [req, res], function() {
            res.send(req.prerender.statusCode || 200);
        });
    }
};

server._send = function(req, res, statusCode, options) {
    var _this = this;

    if(req.prerender.page) {
        req.prerender.page.close();
        req.prerender.page = null;
    }
    req.prerender.stage = 2;

    req.prerender.documentHTML = options || req.prerender.documentHTML;
    req.prerender.statusCode = statusCode || req.prerender.statusCode;

    if(req.prerender.statusCode) {
        req.prerender.statusCode = parseInt(req.prerender.statusCode);
    }

    if (options && typeof options === 'object' && !Buffer.isBuffer(options)) {
        req.prerender.documentHTML = options.documentHTML;
        req.prerender.redirectURL = options.redirectURL;
    }

    this._pluginEvent("beforeSend", [req, res], function() {
        if (req.prerender.redirectURL && !(_this.options.followRedirect || process.env.FOLLOW_REDIRECT)) {
            res.setHeader('Location', req.prerender.redirectURL);
        }

        res.setHeader('Content-Type', 'text/html;charset=UTF-8');

        if(req.headers['accept-encoding'] && req.headers['accept-encoding'].indexOf('gzip') >= 0) {
            res.setHeader('Content-Encoding', 'gzip');
            zlib.gzip(req.prerender.documentHTML, function(err, result) {
                req.prerender.documentHTML = result;
                _this._sendResponse.apply(_this, [req, res, options]);
            });
        } else {
            _this._sendResponse.apply(_this, [req, res, options]);
        }
    });
};

server._sendResponse = function(req, res, options) {

    if (req.prerender.documentHTML) {
        if(Buffer.isBuffer(req.prerender.documentHTML)) {
            res.setHeader('Content-Length', req.prerender.documentHTML.length);
        } else {
            res.setHeader('Content-Length', Buffer.byteLength(req.prerender.documentHTML, 'utf8'));
        }
    }

    res.writeHead(req.prerender.statusCode || 504);

    if (req.prerender.documentHTML) res.write(req.prerender.documentHTML);

    res.end();

    var ms = new Date().getTime() - req.prerender.start.getTime();
    console.log('got', req.prerender.statusCode, 'in', ms + 'ms', 'for', req.prerender.url);

    if(options && options.abort) {
        this.options.worker.kill();
       //  try {
       //     //not happy with this... but when phantomjs is hanging, it can't exit any normal way
       //     console.log('Experiencing infinite javascript loop. Killing phantomjs...');
       //     require('child_process').spawn('pkill', ['phantomjs']);
       //     _this.phantom = null;
       // } catch(e) {
       //     console.log('Error killing phantomjs from javascript infinite loop:', e);
       // }
    }
};
<|endoftext|>