<commit_before>var _ = require('lodash'),
    Q = require('q'),
    async = require('async'),
    pg = require('../db/db').pg,
    util = require('./utilities'),
    geojsonhint = require('geojsonhint');

var NWS = require('./NWS'),
    Node = require('../models/Node'),
    Way = require('../models/Way'),
    Shape = require('../models/Shape'),
    Period = require('../models/Period'),
    Changeset = require('../models/Changeset');

// Normalizes GeoJSON properties to match Atlastory standards
exports.normalize = function(json) {
    if (json.type != 'FeatureCollection') return {};

    function replaceKeys(props) {
        var newJson = {}, newKey;
        for (var key in props) {
            newKey = key.toLowerCase();
            if (newKey == 'abbrev') newKey = 'name_sm';
            if (newKey == 'sovereignt') newKey = 'sovereignty';

            newJson[newKey] = props[key];
        }
        return newJson;
    }

    function replaceValues(p) {
        if (p.name) p._length = p.name.length;
        if (p.type == 'Sovereign country') p.type = 'sovereignty';
        if (p.type == 'Country') p.type = 'country';
        return p;
    }

    json.features.forEach(function(f) {
        var p = f.properties;
        p = replaceKeys(p);
        p = replaceValues(p);
        f.properties = p;
    });

    return json;
};

exports.import = function(options) {
    /* Imports a GeoJSON to the database (NWS format)
     *
     * geojson   OBJECT
     * period    INTEGER
     * type      INTEGER
     * source    INTEGER (default 1)
     * user      INTEGER (default 0)
     * message   STRING  (default 'Import of GeoJSON')
     * checkNode INT     (default 2) check if nodes are duplicated: 0 = don't check; 1 = check duplicates; 2 = check all
     */
    var d = Q.defer(),
        geojson = options.geojson,
        period = parseFloat(options.period),
        type = options.type,
        source = options.source || 1,
        checkNode = (_.isNumber(options.checkNode)) ? options.checkNode : 2;

    if (isNaN(period) || typeof type !== 'number')
        return d.reject(util.err("'period' or 'type' ID missing"));

    var queue = pg.queue(),
        shapeUpdates = [];

    async.map(geojson.features, function(feature, finishFeature) {
        var data = feature.properties;
        // If shape exists, link to new period
        if (checkNode) checkExistingShape(data, function(period) {
            if (period) {
                /*shapeUpdates.push(Shape.update(id, {
                    periods: [['periods || ARRAY['+period.id+'::int8]']],
                    // TODO: change start/end date
                }));
                queue.add(Changeset.Directive.create([['LASTVAL()']], {
                    action: 'link',
                    object: 'shape',
                    object_id: period.id,
                    data: period
                }).returning('changeset_id'));*/
                finishFeature();
            }
            else finishFeature(null, feature);
        });
        else finishFeature(null, feature);

    }, function(err, features) {
        options.geojson.features = _.flatten(features);
        var sql = exports.getImportSql(options);
        if (/--\sError/.test(sql)) return d.reject(util.err(sql, 'generating import sql'));

        queue.add(shapeUpdates);
        sql += queue.print();

        pg.query(sql, function(err, cs) {
            if (err) return d.reject(util.err(err, 'error executing SQL'));
            cs = _.compact(_.pluck(cs, 'changeset_id'));
            cs = parseFloat(cs[0]);
            return d.resolve(cs);
        });
    });

    // Check if shape exists already
    function checkExistingShape(data, callback) {
        callback(null);
        // TODO: how to tell if it's the same shape w/o checking nodes?
        /*Shape.select('id').where({
            name: data.name,
            type_id: type
        }, function(err, shapes) {
            if (err) {
                console.error("Failed to check for checkNode shape: "+err);
                callback(null);
            } else if (shapes.length > 0) callback(parseFloat(shapes[0]));
            else callback(null);
        });*/
    }

    return d.promise;
};
exports.import = util.addCallbackTo(exports.import);

// Generates raw SQL dump
exports.getImportSql = function(options) {
    // Same options as #import()

    var geojson = options.geojson,
        period = parseFloat(options.period),
        type = options.type,
        source = options.source || 1,
        checkNode = (_.isNumber(options.checkNode)) ? options.checkNode : 2,
        Period = require('../models/Period');

    if (isNaN(period) || typeof type !== 'number') return '';

    var changeset = {
        user_id: options.user || 0,
        message: options.message || 'Import of GeoJSON',
        directives: []
    };

    // (1) Parse string & lint GeoJSON

    if (typeof geojson === 'string') geojson = JSON.parse(geojson);
    var geojsonErrors = geojsonhint.hint(JSON.stringify(geojson));
    if (geojsonErrors.length) {
        return [
            '-- Error Importing GeoJSON:',
            '-- GeoJSON has ' + geojsonErrors.length + ' error(s)',
            '-- line ' + geojsonErrors[0].line + ': ' + geojsonErrors[0].message
        ].join('\n');
    }

    // (2) Normalize GeoJSON properties

    geojson = exports.normalize(geojson);

    // (3a) Start SQL dump

    var sql = '';
    var startSql = pg.queue();
    var tempData = _.uniqueId('data_');
    var Data = pg.model(tempData, { idAttribute: 'value' });
    startSql.add('CREATE TEMP TABLE ' + tempData + ' (' +
        'key varchar, ' +
        'value varchar' +
    ') ON COMMIT DROP')
         .add(Changeset.insert(_.omit(changeset, 'directives')))
         .add(Data.insert({ key: 'changeset', value: [['LASTVAL()']] }));
    var changesetId = [['('+Data.select('value::bigint').where({ key: 'changeset' })+')']];
    sql += startSql.print();

    // (3b) Parse each GeoJSON feature

    var duplicateNodes = util.getDuplicateNodes(geojson.features);

    // Check each shape before generating SQL
    geojson.features.map(function(feature) {
        feature.properties = _.extend(feature.properties, {
            periods: [period],
            type_id: type
        });
        return feature;
    }).forEach(function(feature) {
        sql += generateShapeSql(feature);
    });

    return sql;

    // Maps nested coords into array of Way objects
    function getPolyWays(geom) {
        var coords = geom.coordinates;
        if (_.isNumber(coords[0][0])) coords = [coords];
        if (_.isNumber(coords[0][0][0])) coords = [coords];
        return _.flatten(coords.map(function(poly) {
            return poly.map(function(way, i) {
                var isInner = (i !== 0 && /Poly/.test(geom.type));
                var role = isInner ? 'inner' : 'outer';
                role = (/Line/.test(geom.type)) ? 'line' : role;
                return { coords: way, role: role };
            });
        }));
    }

    // Add feature to NWS structure using SQL dump
    function generateShapeSql(feature) {
        var data = feature.properties,
            geom = feature.geometry,
            coords = geom.coordinates,
            esc = pg.engine.escape;

        var nodeData = {
            source_id: source
        };

        var queue = pg.queue();

        // Create temporary table to store shape relations
        var temp = _.uniqueId('rels_');
        queue.add('CREATE TEMP TABLE '+temp+' (' +
            'shape_id bigint, ' +
            'relation_type nws_enum, ' +
            'relation_id bigint, ' +
            'relation_role varchar, ' +
            'sequence_id int' +
        ') ON COMMIT DROP');

        var Temp = pg.model(temp, { idAttribute: 'sequence_id' });

        // Points
        if (/Point/.test(geom.type)) {

            coords = _.isNumber(coords[0]) ? [coords] : coords;
            coords = _.compact(coords.map(function(node) {
                if (!util.verifyCoord(node)) return null;
                return 'create_node(' + [
                    esc(node[0]), esc(node[1]),
                    esc(nodeData.source_id), esc(nodeData.tile)
                ].join() + ')';
            })).map(function(cn, i) {
                return {
                    relation_type: 'Node',
                    relation_id: [[cn]],
                    relation_role: 'point',
                    sequence_id: i
                };
            });
            if (_.isEmpty(coords)) return '';
            queue.add(Temp.insert(coords));

        // Lines & Polygons
        } else {

            // Create ways
            var ways = getPolyWays(geom);
            queue.add(Temp.insert(ways.map(function(way, seq) {
                return {
                    relation_type: 'Way',
                    relation_id: [['cw()']],
                    relation_role: way.role,
                    sequence_id: seq
                };
            })));

            // Create nodes, wayNodes
            queue.add(ways.map(function(way, seq) {
                // Remove invalid coords + convert to string
                way.coords = _.compact(way.coords.map(function(coord) {
                    return !util.verifyCoord(coord) ? null : coord;
                }));

                var duplicateIdxs = _.compact(way.coords.map(function(coord, i) {
                    if (_.contains(duplicateNodes, coord+'')) return i+1;
                    return null;
                }));

                way = [
                    checkNode,
                    'ARRAY' + JSON.stringify(way.coords),
                    duplicateIdxs.length ? 'ARRAY' + JSON.stringify(duplicateIdxs) : 'NULL',
                    esc(data.source_id),
                    '(SELECT relation_id FROM '+temp+' WHERE sequence_id = '+seq+')'
                ].join(',');

                return 'SELECT create_way_nodes(' + way + ') AS n';
            }));
        }

        // Get period start/end to record in shape
        var newData = _.clone(data);
        newData.start_year = [['('+Period.find(period).select('start_year')+')']];
        newData.start_month = [['('+Period.find(period).select('start_month')+')']];
        newData.start_day = [['('+Period.find(period).select('start_day')+')']];
        newData.end_year = [['('+Period.find(period).select('end_year')+')']];
        newData.end_month = [['('+Period.find(period).select('end_month')+')']];
        newData.end_day = [['('+Period.find(period).select('end_day')+')']];

        // Create shape & update with new shape_id
        queue.add(Shape.create(newData)).add(Temp.update({
            shape_id: [['LASTVAL()']]
        }));

        // Finish shape relations
        queue.add(Shape.Relation.insert(Temp.all()).returning('shape_id'));

        queue.add(Changeset.Directive.create(changesetId, {
            action: 'add',
            object: 'shape',
            object_id: Temp.select('shape_id').limit(1),
            data: JSON.stringify(data),
            geometry: { type: geom.type }
        }).returning('changeset_id'));

        return '\n\n-- ' + data.name + '\n' + queue.print();
    }
};

///////////////////////////////////////////

exports.export = function(options) {
    /* Exports to GeoJSON
     *
     * period    INT
     * type      INT|INT[]
     * bbox      INT[]   (optional) [west, south, east, north]
     * detail    INTEGER (default 100%)
     * withData  BOOLEAN (default true)
     */

    var d = Q.defer(),
        period = parseFloat(options.period),
        type = options.type,
        bbox = options.bbox || null,
        detail = options.detail || 1,
        withData = (typeof options.withData === 'boolean') ? options.withData : true;

    if (_.isNumber(type)) type = [type];
    if (isNaN(period) || !_.isArray(type)) return d.reject(util.err("'period' or 'type' ID invalid", "exporting GeoJSON"));

    var cols = withData ? "*" : ["id", "name"];

    var geoJSON = {
        "type": "FeatureCollection",
        "features": []
    };

    async.parallel({ properties: function(done) {
        Shape.getData({ period: period, type: type }).select(cols).run(done);
    }, nodes: function(done) {
        Shape.getNodes({ period: period, type: type, box: bbox }).run(done);
    }}, function(err, res) {
        if (err) return d.reject(util.err(err, "exporting GeoJSON"));

        geoJSON.features = _(res.nodes).groupBy(function(node) {
            return node.shape;
        }).map(function(shape, id) {
            return {
                "type": "Feature",
                "properties": _.find(res.properties, { 'id': id }).toJSON(),
                "geometry": geometryFromNodes(shape)
            };
        }).value();

        d.resolve(geoJSON);
    });

    function geometryFromNodes(nodes) {
        var ways = _.groupBy(nodes, function(node) { return node.way; });
        ways = _.sortBy(ways, function(way) { return way[0].seq1; });

        var coords = [], type;
        var nodeCount = nodes.length;
        var wayCount = ways.length;
        var outerCount = _.filter(ways, function(w) { return w[0].role == 'outer'; }).length;
        var firstRole = nodes[0].role;

        if (!outerCount && nodeCount == 1) {
            type = 'Point';
            coords = [parseFloat(nodes[0].lon), parseFloat(nodes[0].lat)];

        } else if (!outerCount && nodeCount > 1 && wayCount == 1) {
            type = (firstRole == 'line') ? 'LineString' : 'MultiPoint';
            coords = parseWay(ways[0]);

        } else if (wayCount > 1 && firstRole == 'line' || outerCount == 1) {
            type = (outerCount == 1) ? 'Polygon' : 'MultiLineString';
            coords = ways.map(function(way) {
                return parseWay(way);
            });

        } else if (outerCount > 1) {
            type = 'MultiPolygon';
            for (var i in ways) {
                var shape = parseWay(ways[i]);
                if (ways[i][0].role == 'inner') coords[coords.length-1].push(shape);
                else coords.push([shape]);
            }
        }

        return {
            "type": type,
            "coordinates": coords
        };
    }

    function parseWay(nodes) {
        var way = nodes.map(function(node) {
            return [parseFloat(node.lon), parseFloat(node.lat)];
        });

        // TODO: create nodes based on provided bbox

        // Make sure polys match first/last coords
        var role = nodes[0].role;
        if (role == 'inner' || role == 'outer') {
            if (way[0].toString() != way[way.length-1].toString())
                way.push(way[0]);
        }

        return way;
    }

    return d.promise;
};
exports.export = util.addCallbackTo(exports.export);
<commit_msg>Fix geojson export name column<commit_after>var _ = require('lodash'),
    Q = require('q'),
    async = require('async'),
    pg = require('../db/db').pg,
    util = require('./utilities'),
    geojsonhint = require('geojsonhint');

var NWS = require('./NWS'),
    Node = require('../models/Node'),
    Way = require('../models/Way'),
    Shape = require('../models/Shape'),
    Period = require('../models/Period'),
    Changeset = require('../models/Changeset');

// Normalizes GeoJSON properties to match Atlastory standards
exports.normalize = function(json) {
    if (json.type != 'FeatureCollection') return {};

    function replaceKeys(props) {
        var newJson = {}, newKey;
        for (var key in props) {
            newKey = key.toLowerCase();
            if (newKey == 'abbrev') newKey = 'name_sm';
            if (newKey == 'sovereignt') newKey = 'sovereignty';

            newJson[newKey] = props[key];
        }
        return newJson;
    }

    function replaceValues(p) {
        if (p.name) p._length = p.name.length;
        if (p.type == 'Sovereign country') p.type = 'sovereignty';
        if (p.type == 'Country') p.type = 'country';
        return p;
    }

    json.features.forEach(function(f) {
        var p = f.properties;
        p = replaceKeys(p);
        p = replaceValues(p);
        f.properties = p;
    });

    return json;
};

exports.import = function(options) {
    /* Imports a GeoJSON to the database (NWS format)
     *
     * geojson   OBJECT
     * period    INTEGER
     * type      INTEGER
     * source    INTEGER (default 1)
     * user      INTEGER (default 0)
     * message   STRING  (default 'Import of GeoJSON')
     * checkNode INT     (default 2) check if nodes are duplicated: 0 = don't check; 1 = check duplicates; 2 = check all
     */
    var d = Q.defer(),
        geojson = options.geojson,
        period = parseFloat(options.period),
        type = options.type,
        source = options.source || 1,
        checkNode = (_.isNumber(options.checkNode)) ? options.checkNode : 2;

    if (isNaN(period) || typeof type !== 'number')
        return d.reject(util.err("'period' or 'type' ID missing"));

    var queue = pg.queue(),
        shapeUpdates = [];

    async.map(geojson.features, function(feature, finishFeature) {
        var data = feature.properties;
        // If shape exists, link to new period
        if (checkNode) checkExistingShape(data, function(period) {
            if (period) {
                /*shapeUpdates.push(Shape.update(id, {
                    periods: [['periods || ARRAY['+period.id+'::int8]']],
                    // TODO: change start/end date
                }));
                queue.add(Changeset.Directive.create([['LASTVAL()']], {
                    action: 'link',
                    object: 'shape',
                    object_id: period.id,
                    data: period
                }).returning('changeset_id'));*/
                finishFeature();
            }
            else finishFeature(null, feature);
        });
        else finishFeature(null, feature);

    }, function(err, features) {
        options.geojson.features = _.flatten(features);
        var sql = exports.getImportSql(options);
        if (/--\sError/.test(sql)) return d.reject(util.err(sql, 'generating import sql'));

        queue.add(shapeUpdates);
        sql += queue.print();

        pg.query(sql, function(err, cs) {
            if (err) return d.reject(util.err(err, 'error executing SQL'));
            cs = _.compact(_.pluck(cs, 'changeset_id'));
            cs = parseFloat(cs[0]);
            return d.resolve(cs);
        });
    });

    // Check if shape exists already
    function checkExistingShape(data, callback) {
        callback(null);
        // TODO: how to tell if it's the same shape w/o checking nodes?
        /*Shape.select('id').where({
            name: data.name,
            type_id: type
        }, function(err, shapes) {
            if (err) {
                console.error("Failed to check for checkNode shape: "+err);
                callback(null);
            } else if (shapes.length > 0) callback(parseFloat(shapes[0]));
            else callback(null);
        });*/
    }

    return d.promise;
};
exports.import = util.addCallbackTo(exports.import);

// Generates raw SQL dump
exports.getImportSql = function(options) {
    // Same options as #import()

    var geojson = options.geojson,
        period = parseFloat(options.period),
        type = options.type,
        source = options.source || 1,
        checkNode = (_.isNumber(options.checkNode)) ? options.checkNode : 2,
        Period = require('../models/Period');

    if (isNaN(period) || typeof type !== 'number') return '';

    var changeset = {
        user_id: options.user || 0,
        message: options.message || 'Import of GeoJSON',
        directives: []
    };

    // (1) Parse string & lint GeoJSON

    if (typeof geojson === 'string') geojson = JSON.parse(geojson);
    var geojsonErrors = geojsonhint.hint(JSON.stringify(geojson));
    if (geojsonErrors.length) {
        return [
            '-- Error Importing GeoJSON:',
            '-- GeoJSON has ' + geojsonErrors.length + ' error(s)',
            '-- line ' + geojsonErrors[0].line + ': ' + geojsonErrors[0].message
        ].join('\n');
    }

    // (2) Normalize GeoJSON properties

    geojson = exports.normalize(geojson);

    // (3a) Start SQL dump

    var sql = '';
    var startSql = pg.queue();
    var tempData = _.uniqueId('data_');
    var Data = pg.model(tempData, { idAttribute: 'value' });
    startSql.add('CREATE TEMP TABLE ' + tempData + ' (' +
        'key varchar, ' +
        'value varchar' +
    ') ON COMMIT DROP')
         .add(Changeset.insert(_.omit(changeset, 'directives')))
         .add(Data.insert({ key: 'changeset', value: [['LASTVAL()']] }));
    var changesetId = [['('+Data.select('value::bigint').where({ key: 'changeset' })+')']];
    sql += startSql.print();

    // (3b) Parse each GeoJSON feature

    var duplicateNodes = util.getDuplicateNodes(geojson.features);

    // Check each shape before generating SQL
    geojson.features.map(function(feature) {
        feature.properties = _.extend(feature.properties, {
            periods: [period],
            type_id: type
        });
        return feature;
    }).forEach(function(feature) {
        sql += generateShapeSql(feature);
    });

    return sql;

    // Maps nested coords into array of Way objects
    function getPolyWays(geom) {
        var coords = geom.coordinates;
        if (_.isNumber(coords[0][0])) coords = [coords];
        if (_.isNumber(coords[0][0][0])) coords = [coords];
        return _.flatten(coords.map(function(poly) {
            return poly.map(function(way, i) {
                var isInner = (i !== 0 && /Poly/.test(geom.type));
                var role = isInner ? 'inner' : 'outer';
                role = (/Line/.test(geom.type)) ? 'line' : role;
                return { coords: way, role: role };
            });
        }));
    }

    // Add feature to NWS structure using SQL dump
    function generateShapeSql(feature) {
        var data = feature.properties,
            geom = feature.geometry,
            coords = geom.coordinates,
            esc = pg.engine.escape;

        var nodeData = {
            source_id: source
        };

        var queue = pg.queue();

        // Create temporary table to store shape relations
        var temp = _.uniqueId('rels_');
        queue.add('CREATE TEMP TABLE '+temp+' (' +
            'shape_id bigint, ' +
            'relation_type nws_enum, ' +
            'relation_id bigint, ' +
            'relation_role varchar, ' +
            'sequence_id int' +
        ') ON COMMIT DROP');

        var Temp = pg.model(temp, { idAttribute: 'sequence_id' });

        // Points
        if (/Point/.test(geom.type)) {

            coords = _.isNumber(coords[0]) ? [coords] : coords;
            coords = _.compact(coords.map(function(node) {
                if (!util.verifyCoord(node)) return null;
                return 'create_node(' + [
                    esc(node[0]), esc(node[1]),
                    esc(nodeData.source_id), esc(nodeData.tile)
                ].join() + ')';
            })).map(function(cn, i) {
                return {
                    relation_type: 'Node',
                    relation_id: [[cn]],
                    relation_role: 'point',
                    sequence_id: i
                };
            });
            if (_.isEmpty(coords)) return '';
            queue.add(Temp.insert(coords));

        // Lines & Polygons
        } else {

            // Create ways
            var ways = getPolyWays(geom);
            queue.add(Temp.insert(ways.map(function(way, seq) {
                return {
                    relation_type: 'Way',
                    relation_id: [['cw()']],
                    relation_role: way.role,
                    sequence_id: seq
                };
            })));

            // Create nodes, wayNodes
            queue.add(ways.map(function(way, seq) {
                // Remove invalid coords + convert to string
                way.coords = _.compact(way.coords.map(function(coord) {
                    return !util.verifyCoord(coord) ? null : coord;
                }));

                var duplicateIdxs = _.compact(way.coords.map(function(coord, i) {
                    if (_.contains(duplicateNodes, coord+'')) return i+1;
                    return null;
                }));

                way = [
                    checkNode,
                    'ARRAY' + JSON.stringify(way.coords),
                    duplicateIdxs.length ? 'ARRAY' + JSON.stringify(duplicateIdxs) : 'NULL',
                    esc(data.source_id),
                    '(SELECT relation_id FROM '+temp+' WHERE sequence_id = '+seq+')'
                ].join(',');

                return 'SELECT create_way_nodes(' + way + ') AS n';
            }));
        }

        // Get period start/end to record in shape
        var newData = _.clone(data);
        newData.start_year = [['('+Period.find(period).select('start_year')+')']];
        newData.start_month = [['('+Period.find(period).select('start_month')+')']];
        newData.start_day = [['('+Period.find(period).select('start_day')+')']];
        newData.end_year = [['('+Period.find(period).select('end_year')+')']];
        newData.end_month = [['('+Period.find(period).select('end_month')+')']];
        newData.end_day = [['('+Period.find(period).select('end_day')+')']];

        // Create shape & update with new shape_id
        queue.add(Shape.create(newData)).add(Temp.update({
            shape_id: [['LASTVAL()']]
        }));

        // Finish shape relations
        queue.add(Shape.Relation.insert(Temp.all()).returning('shape_id'));

        queue.add(Changeset.Directive.create(changesetId, {
            action: 'add',
            object: 'shape',
            object_id: Temp.select('shape_id').limit(1),
            data: JSON.stringify(data),
            geometry: { type: geom.type }
        }).returning('changeset_id'));

        return '\n\n-- ' + data.name + '\n' + queue.print();
    }
};

///////////////////////////////////////////

exports.export = function(options) {
    /* Exports to GeoJSON
     *
     * period    INT
     * type      INT|INT[]
     * bbox      INT[]   (optional) [west, south, east, north]
     * detail    INTEGER (default 100%)
     * withData  BOOLEAN (default true)
     */

    var d = Q.defer(),
        period = parseFloat(options.period),
        type = options.type,
        bbox = options.bbox || null,
        detail = options.detail || 1,
        withData = (typeof options.withData === 'boolean') ? options.withData : true;

    if (_.isNumber(type)) type = [type];
    if (isNaN(period) || !_.isArray(type)) return d.reject(util.err("'period' or 'type' ID invalid", "exporting GeoJSON"));

    var cols = withData ? "*" : ["id"];

    var geoJSON = {
        "type": "FeatureCollection",
        "features": []
    };

    async.parallel({ properties: function(done) {
        Shape.getData({ period: period, type: type }).select(cols).run(done);
    }, nodes: function(done) {
        Shape.getNodes({ period: period, type: type, box: bbox }).run(done);
    }}, function(err, res) {
        if (err) return d.reject(util.err(err, "exporting GeoJSON"));

        geoJSON.features = _(res.nodes).groupBy(function(node) {
            return node.shape;
        }).map(function(shape, id) {
            return {
                "type": "Feature",
                "properties": _.find(res.properties, { 'id': id }).toJSON(),
                "geometry": geometryFromNodes(shape)
            };
        }).value();

        d.resolve(geoJSON);
    });

    function geometryFromNodes(nodes) {
        var ways = _.groupBy(nodes, function(node) { return node.way; });
        ways = _.sortBy(ways, function(way) { return way[0].seq1; });

        var coords = [], type;
        var nodeCount = nodes.length;
        var wayCount = ways.length;
        var outerCount = _.filter(ways, function(w) { return w[0].role == 'outer'; }).length;
        var firstRole = nodes[0].role;

        if (!outerCount && nodeCount == 1) {
            type = 'Point';
            coords = [parseFloat(nodes[0].lon), parseFloat(nodes[0].lat)];

        } else if (!outerCount && nodeCount > 1 && wayCount == 1) {
            type = (firstRole == 'line') ? 'LineString' : 'MultiPoint';
            coords = parseWay(ways[0]);

        } else if (wayCount > 1 && firstRole == 'line' || outerCount == 1) {
            type = (outerCount == 1) ? 'Polygon' : 'MultiLineString';
            coords = ways.map(function(way) {
                return parseWay(way);
            });

        } else if (outerCount > 1) {
            type = 'MultiPolygon';
            for (var i in ways) {
                var shape = parseWay(ways[i]);
                if (ways[i][0].role == 'inner') coords[coords.length-1].push(shape);
                else coords.push([shape]);
            }
        }

        return {
            "type": type,
            "coordinates": coords
        };
    }

    function parseWay(nodes) {
        var way = nodes.map(function(node) {
            return [parseFloat(node.lon), parseFloat(node.lat)];
        });

        // TODO: create nodes based on provided bbox

        // Make sure polys match first/last coords
        var role = nodes[0].role;
        if (role == 'inner' || role == 'outer') {
            if (way[0].toString() != way[way.length-1].toString())
                way.push(way[0]);
        }

        return way;
    }

    return d.promise;
};
exports.export = util.addCallbackTo(exports.export);
<|endoftext|>


<commit_before>module.exports = {
	events: {
		/* TODO: fix dupe bot var */
		message: function ( bot, nick, to, text ) {
			var pc = text.match( /(ed balls|the greater good|now we know|8|eight)+/gi );
			if ( pc && pc.length !== 0 ) {
				bot.shout( to, pc.join( ' ' ) );
			}
		}
	}
};
<commit_msg>Remove 8<commit_after>module.exports = {
	events: {
		/* TODO: fix dupe bot var */
		message: function ( bot, nick, to, text ) {
			var pc = text.match( /(ed balls|the greater good|now we know|eight)+/gi );
			if ( pc && pc.length !== 0 ) {
				bot.shout( to, pc.join( ' ' ) );
			}
		}
	}
};
<|endoftext|>


<commit_before>'use strict';

var get = require('lodash.get');
var plurals = require('./plurals');

module.exports = Gettext;

/**
 * Creates and returns a new Gettext instance.
 *
 * @constructor
 * @param  {Object}  options        A set of options
 * @param  {Boolean} options.debug  Whether to output debug info into the
 *                                  console.
 * @return {Object}  A Gettext instance
 */
function Gettext(options) {
    options = options || {};

    this.catalogs = {};
    this.locale = null;
    this.domain = 'messages';

    // Set debug flag
    if ('debug' in options) {
        this.debug = options.debug === true;
    }
    else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) {
        this.debug = process.env.NODE_ENV !== 'production';
    }
    else {
        this.debug = false;
    }
}

/**
 * Logs a warning to the console if debug mode is enabled.
 *
 * @ignore
 * @param  {String} message  A warning message
 */
Gettext.prototype.warn = function(message) {
    if (this.debug) {
        console.warn(message);
    }
};

/**
 * Stores a set of translations in the set of gettext
 * catalogs.
 *
 * @example
 *     gt.addTranslations('sv-SE', 'messages', translationsObject)
 *
 * @param {String} locale        A locale string
 * @param {String} domain        A domain name
 * @param {Object} translations  An object of gettext-parser JSON shape
 */
Gettext.prototype.addTranslations = function(locale, domain, translations) {
    if (!this.catalogs[locale]) {
        this.catalogs[locale] = {};
    }

    this.catalogs[locale][domain] = translations;
};

/**
 * Sets the locale to get translated messages for.
 *
 * @example
 *     gt.setLocale('sv-SE')
 *
 * @param {String} locale  A locale
 */
Gettext.prototype.setLocale = function(locale) {
    if (!locale) {
        this.warn('You called setLocale() with an empty value, which makes little sense.');
        return;
    }

    if (!this.catalogs[locale]) {
        this.warn('You called setLocale() with "' + locale + '", but no translations for that locale has been added.');
        return;
    }

    this.locale = locale;
};

/**
 * Sets the default gettext domain.
 *
 * @example
 *     gt.setTextDomain('domainname')
 *
 * @param {String} domain  A gettext domain name
 */
Gettext.prototype.setTextDomain = function(domain) {
    if (!domain) {
        this.warn('You called setTextDomain() with an empty `domain` value, which is not allowed.');
        return;
    }

    this.domain = domain;
};

/**
 * Translates a string using the default textdomain
 *
 * @example
 *     gt.gettext('Some text')
 *
 * @param {String} msgid  String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.gettext = function(msgid) {
    return this.dnpgettext(this.domain, '', msgid);
};

/**
 * Translates a string using a specific domain
 *
 * @example
 *     gt.dgettext('domainname', 'Some text')
 *
 * @param  {String} domain  A gettext domain name
 * @param  {String} msgid   String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dgettext = function(domain, msgid) {
    return this.dnpgettext(domain, '', msgid);
};

/**
 * Translates a plural string using the default textdomain
 *
 * @example
 *     gt.ngettext('One thing', 'Many things', numberOfThings)
 *
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.ngettext = function(msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a plural string using a specific textdomain
 *
 * @example
 *     gt.dngettext('domainname', One thing', 'Many things', numberOfThings)
 *
 * @param {String} domain       A gettext domain name
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dngettext = function(domain, msgid, msgidPlural, count) {
    return this.dnpgettext(domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a string from a specific context using the default textdomain
 *
 * @example
 *    gt.pgettext('sports', 'Back')
 *
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.pgettext = function(msgctxt, msgid) {
    return this.dnpgettext(this.domain, msgctxt, msgid);
};

/**
 * Translates a string from a specific context using s specific textdomain
 *
 * @example
 *     gt.dpgettext('domainname', 'sports', 'Back')
 *
 * @param {String} domain   A gettext domain name
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dpgettext = function(domain, msgctxt, msgid) {
    return this.dnpgettext(domain, msgctxt, msgid);
};

/**
 * Translates a plural string from a specific context using the default textdomain
 *
 * @example
 *     gt.npgettext('sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param {String} msgctxt      Translation context
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.npgettext = function(msgctxt, msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, msgctxt, msgid, msgidPlural, count);
};

/**
 * Translates a plural string from a specifi context using a specific textdomain
 *
 * @example
 *     gt.dnpgettext('domainname', 'sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param {String} domain       A gettext domain name
 * @param {String} msgctxt      Translation context
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dnpgettext = function(domain, msgctxt, msgid, msgidPlural, count) {
    var defaultTranslation = msgid;
    var translation;
    var index;

    msgctxt = msgctxt || '';

    if (!isNaN(count) && count !== 1) {
        defaultTranslation = msgidPlural || msgid;
    }

    if (!this.locale) {
        this.warn('You need to set a locale using setLocale(locale) before getting translated messages.');
        return defaultTranslation;
    }

    translation = this._getTranslation(domain, msgctxt, msgid);

    if (translation) {
        if (typeof count === 'number') {
            var pluralsFunc = plurals[Gettext.getLanguageCode(this.locale)].pluralsFunc;
            index = pluralsFunc(count);
            if (typeof index === 'boolean') {
                index = index ? 1 : 0;
            }
        } else {
            index = 0;
        }

        return translation.msgstr[index] || defaultTranslation;
    }
    else {
        this.warn('No translation was found for msgid "' + msgid + '" in msgctxt "' + msgctxt + '" and domain "' + domain + '"');
    }

    return defaultTranslation;
};

/**
 * Retrieves comments object for a translation. The comments object
 * has the shape `{ translator, extracted, reference, flag, previous }`.
 *
 * @example
 *     const comment = gt.getComment('domainname', 'sports', 'Backs')
 *
 * @param {String} domain   A gettext domain name
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {Object} Comments object or false if not found
 */
Gettext.prototype.getComment = function(domain, msgctxt, msgid) {
    var translation;

    translation = this._getTranslation(domain, msgctxt, msgid);
    if (translation) {
        return translation.comments || {};
    }

    return {};
};

/**
 * Retrieves translation object from the domain and context
 *
 * @private
 * @param {String} domain   A gettext domain name
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {Object} Translation object or false if not found
 */
Gettext.prototype._getTranslation = function(domain, msgctxt, msgid) {
    msgctxt = msgctxt || '';

    return get(this.catalogs, [this.locale, domain, 'translations', msgctxt, msgid]);
};

/**
 * Returns the language code part of a locale
 *
 * @example
 *     Gettext.getLanguageCode('sv-SE')
 *     // -> "sv"
 *
 * @param {String} locale  A case-insensitive locale string
 * @returns {String} A language code
 */
Gettext.getLanguageCode = function(locale) {
    return locale.split(/[\-_]/)[0].toLowerCase();
};

/**
 * This function will be removed in the final 2.0.0 release.
 *
 * @deprecated
 */
Gettext.prototype.addTextdomain = function() {

    // TODO(alexanderwallin): Add instructions for file i/o
    console.error('addTextdomain() is deprecated.\n\n' +
        '* To add translations, use addTranslations()\n' +
        '* To set the default domain, use setTextDomain()');
};

/**
 * This function will be removed in the final 2.0.0 release.
 *
 * @deprecated
 */
Gettext.prototype.textdomain = function() {
    console.error('textdomain() is deprecated.\n\n' +
        '* To set the current locale, use setLocale()\n' +
        '* To set the default domain, use setTextDomain()');
};
<commit_msg>Fixes missing single qoute in docs code example<commit_after>'use strict';

var get = require('lodash.get');
var plurals = require('./plurals');

module.exports = Gettext;

/**
 * Creates and returns a new Gettext instance.
 *
 * @constructor
 * @param  {Object}  options        A set of options
 * @param  {Boolean} options.debug  Whether to output debug info into the
 *                                  console.
 * @return {Object}  A Gettext instance
 */
function Gettext(options) {
    options = options || {};

    this.catalogs = {};
    this.locale = null;
    this.domain = 'messages';

    // Set debug flag
    if ('debug' in options) {
        this.debug = options.debug === true;
    }
    else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV) {
        this.debug = process.env.NODE_ENV !== 'production';
    }
    else {
        this.debug = false;
    }
}

/**
 * Logs a warning to the console if debug mode is enabled.
 *
 * @ignore
 * @param  {String} message  A warning message
 */
Gettext.prototype.warn = function(message) {
    if (this.debug) {
        console.warn(message);
    }
};

/**
 * Stores a set of translations in the set of gettext
 * catalogs.
 *
 * @example
 *     gt.addTranslations('sv-SE', 'messages', translationsObject)
 *
 * @param {String} locale        A locale string
 * @param {String} domain        A domain name
 * @param {Object} translations  An object of gettext-parser JSON shape
 */
Gettext.prototype.addTranslations = function(locale, domain, translations) {
    if (!this.catalogs[locale]) {
        this.catalogs[locale] = {};
    }

    this.catalogs[locale][domain] = translations;
};

/**
 * Sets the locale to get translated messages for.
 *
 * @example
 *     gt.setLocale('sv-SE')
 *
 * @param {String} locale  A locale
 */
Gettext.prototype.setLocale = function(locale) {
    if (!locale) {
        this.warn('You called setLocale() with an empty value, which makes little sense.');
        return;
    }

    if (!this.catalogs[locale]) {
        this.warn('You called setLocale() with "' + locale + '", but no translations for that locale has been added.');
        return;
    }

    this.locale = locale;
};

/**
 * Sets the default gettext domain.
 *
 * @example
 *     gt.setTextDomain('domainname')
 *
 * @param {String} domain  A gettext domain name
 */
Gettext.prototype.setTextDomain = function(domain) {
    if (!domain) {
        this.warn('You called setTextDomain() with an empty `domain` value, which is not allowed.');
        return;
    }

    this.domain = domain;
};

/**
 * Translates a string using the default textdomain
 *
 * @example
 *     gt.gettext('Some text')
 *
 * @param {String} msgid  String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.gettext = function(msgid) {
    return this.dnpgettext(this.domain, '', msgid);
};

/**
 * Translates a string using a specific domain
 *
 * @example
 *     gt.dgettext('domainname', 'Some text')
 *
 * @param  {String} domain  A gettext domain name
 * @param  {String} msgid   String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dgettext = function(domain, msgid) {
    return this.dnpgettext(domain, '', msgid);
};

/**
 * Translates a plural string using the default textdomain
 *
 * @example
 *     gt.ngettext('One thing', 'Many things', numberOfThings)
 *
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.ngettext = function(msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a plural string using a specific textdomain
 *
 * @example
 *     gt.dngettext('domainname', 'One thing', 'Many things', numberOfThings)
 *
 * @param {String} domain       A gettext domain name
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dngettext = function(domain, msgid, msgidPlural, count) {
    return this.dnpgettext(domain, '', msgid, msgidPlural, count);
};

/**
 * Translates a string from a specific context using the default textdomain
 *
 * @example
 *    gt.pgettext('sports', 'Back')
 *
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.pgettext = function(msgctxt, msgid) {
    return this.dnpgettext(this.domain, msgctxt, msgid);
};

/**
 * Translates a string from a specific context using s specific textdomain
 *
 * @example
 *     gt.dpgettext('domainname', 'sports', 'Back')
 *
 * @param {String} domain   A gettext domain name
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dpgettext = function(domain, msgctxt, msgid) {
    return this.dnpgettext(domain, msgctxt, msgid);
};

/**
 * Translates a plural string from a specific context using the default textdomain
 *
 * @example
 *     gt.npgettext('sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param {String} msgctxt      Translation context
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.npgettext = function(msgctxt, msgid, msgidPlural, count) {
    return this.dnpgettext(this.domain, msgctxt, msgid, msgidPlural, count);
};

/**
 * Translates a plural string from a specifi context using a specific textdomain
 *
 * @example
 *     gt.dnpgettext('domainname', 'sports', 'Back', '%d backs', numberOfBacks)
 *
 * @param {String} domain       A gettext domain name
 * @param {String} msgctxt      Translation context
 * @param {String} msgid        String to be translated
 * @param {String} msgidPlural  If no translation was found, return this on count!=1
 * @param {Number} count        Number count for the plural
 * @return {String} Translation or the original string if no translation was found
 */
Gettext.prototype.dnpgettext = function(domain, msgctxt, msgid, msgidPlural, count) {
    var defaultTranslation = msgid;
    var translation;
    var index;

    msgctxt = msgctxt || '';

    if (!isNaN(count) && count !== 1) {
        defaultTranslation = msgidPlural || msgid;
    }

    if (!this.locale) {
        this.warn('You need to set a locale using setLocale(locale) before getting translated messages.');
        return defaultTranslation;
    }

    translation = this._getTranslation(domain, msgctxt, msgid);

    if (translation) {
        if (typeof count === 'number') {
            var pluralsFunc = plurals[Gettext.getLanguageCode(this.locale)].pluralsFunc;
            index = pluralsFunc(count);
            if (typeof index === 'boolean') {
                index = index ? 1 : 0;
            }
        } else {
            index = 0;
        }

        return translation.msgstr[index] || defaultTranslation;
    }
    else {
        this.warn('No translation was found for msgid "' + msgid + '" in msgctxt "' + msgctxt + '" and domain "' + domain + '"');
    }

    return defaultTranslation;
};

/**
 * Retrieves comments object for a translation. The comments object
 * has the shape `{ translator, extracted, reference, flag, previous }`.
 *
 * @example
 *     const comment = gt.getComment('domainname', 'sports', 'Backs')
 *
 * @param {String} domain   A gettext domain name
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {Object} Comments object or false if not found
 */
Gettext.prototype.getComment = function(domain, msgctxt, msgid) {
    var translation;

    translation = this._getTranslation(domain, msgctxt, msgid);
    if (translation) {
        return translation.comments || {};
    }

    return {};
};

/**
 * Retrieves translation object from the domain and context
 *
 * @private
 * @param {String} domain   A gettext domain name
 * @param {String} msgctxt  Translation context
 * @param {String} msgid    String to be translated
 * @return {Object} Translation object or false if not found
 */
Gettext.prototype._getTranslation = function(domain, msgctxt, msgid) {
    msgctxt = msgctxt || '';

    return get(this.catalogs, [this.locale, domain, 'translations', msgctxt, msgid]);
};

/**
 * Returns the language code part of a locale
 *
 * @example
 *     Gettext.getLanguageCode('sv-SE')
 *     // -> "sv"
 *
 * @param {String} locale  A case-insensitive locale string
 * @returns {String} A language code
 */
Gettext.getLanguageCode = function(locale) {
    return locale.split(/[\-_]/)[0].toLowerCase();
};

/**
 * This function will be removed in the final 2.0.0 release.
 *
 * @deprecated
 */
Gettext.prototype.addTextdomain = function() {

    // TODO(alexanderwallin): Add instructions for file i/o
    console.error('addTextdomain() is deprecated.\n\n' +
        '* To add translations, use addTranslations()\n' +
        '* To set the default domain, use setTextDomain()');
};

/**
 * This function will be removed in the final 2.0.0 release.
 *
 * @deprecated
 */
Gettext.prototype.textdomain = function() {
    console.error('textdomain() is deprecated.\n\n' +
        '* To set the current locale, use setLocale()\n' +
        '* To set the default domain, use setTextDomain()');
};
<|endoftext|>


<commit_before>var networking = require("./networking");
var logging = require("./logging");
var config = require("./config");
var cache = require("./cache");
var skins = require("./skins");
var renders = require("./renders");
var fs = require("fs");

// 0098cb60-fa8e-427c-b299-793cbd302c9a
var valid_user_id = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username
var hash_pattern = /[0-9a-f]+$/;

// gets the hash from the textures.minecraft.net +url+
function get_hash(url) {
  return hash_pattern.exec(url)[0].toLowerCase();
}

function store_skin(rid, userId, profile, details, callback) {
  networking.get_skin_url(rid, userId, profile, function(url) {
    if (url) {
      var skin_hash = get_hash(url);
      if (details && details.skin === skin_hash) {
        cache.update_timestamp(rid, userId, skin_hash, function(err) {
          callback(err, skin_hash);
        });
      } else {
        logging.log(rid + "new skin hash: " + skin_hash);
        var facepath = __dirname + "/../" + config.faces_dir + skin_hash + ".png";
        var helmpath = __dirname + "/../" + config.helms_dir + skin_hash + ".png";
        fs.exists(facepath, function(exists) {
          if (exists) {
            logging.log(rid + "skin already exists, not downloading");
            callback(null, skin_hash);
          } else {
            networking.get_from(rid, url, function(img, response, err) {
              if (err || !img) {
                callback(err, null);
              } else {
                skins.extract_face(img, facepath, function(err) {
                  if (err) {
                    logging.error(rid + err.stack);
                    callback(err, null);
                  } else {
                    logging.log(rid + "face extracted");
                    skins.extract_helm(rid, facepath, img, helmpath, function(err) {
                      logging.log(rid + "helm extracted");
                      logging.debug(rid + helmpath);
                      callback(err, skin_hash);
                    });
                  }
                });
              }
            });
          }
        });
      }
    } else {
      callback(null, null);
    }
  });
}

function store_cape(rid, userId, profile, details, callback) {
  networking.get_cape_url(rid, userId, profile, function(url) {
    if (url) {
      var cape_hash = get_hash(url);
      if (details && details.cape === cape_hash) {
        cache.update_timestamp(rid, userId, cape_hash, function(err) {
          callback(err, cape_hash);
        });
      } else {
        logging.log(rid + "new cape hash: " + cape_hash);
        var capepath = __dirname + "/../" + config.capes_dir + cape_hash + ".png";
        fs.exists(capepath, function(exists) {
          if (exists) {
            logging.log(rid + "cape already exists, not downloading");
            callback(null, cape_hash);
          } else {
            networking.get_from(rid, url, function(img, response, err) {
              if (err || !img) {
                logging.error(rid + err.stack);
                callback(err, null);
              } else {
                skins.save_image(img, capepath, function(err) {
                  logging.log(rid + "cape saved");
                  callback(err, cape_hash);
                });
              }
            });
          }
        });
      }
    } else {
      callback(null, null);
    }
  });
}

// used by store_images to queue simultaneous requests for identical userId
// the first request has to be completed until all others are continued
var currently_running = [];
// calls back all queued requests that match userId and type
function callback_for(userId, type, err, hash) {
  var req_count = 0;
  for (var i = 0; i < currently_running.length; i++) {
    var current = currently_running[i];
    if (current.userid === userId && current.type === type) {
      req_count++;
      if (req_count !== 1) {
        // otherwise this would show up on single/first requests, too
        logging.debug(current.rid + "queued " + type + " request continued");
      }
      currently_running.splice(i, 1); // remove from array
      current.callback(err, hash);
      i--;
    }
  }
  if (req_count > 1) {
    logging.debug(req_count + " simultaneous requests for " + userId);
  }
}

// returns true if any object in +arr+ has +value+ as +property+
function deep_property_check(arr, property, value) {
  for (var i = 0; i < arr.length; i++) {
    if (arr[i][property] === value) {
      return true;
    }
  }
  return false;
}

// downloads the images for +userId+ while checking the cache
// status based on +details+. +type+ specifies which
// image type should be called back on
// +callback+ contains error, image hash
function store_images(rid, userId, details, type, callback) {
  var is_uuid = userId.length > 16;
  var new_hash = {
    rid: rid,
    userid: userId,
    type: type,
    callback: callback
  };
  if (!deep_property_check(currently_running, "userid", userId)) {
    currently_running.push(new_hash);
    networking.get_profile(rid, (is_uuid ? userId : null), function(err, profile) {
      if (err || (is_uuid && !profile)) {
        if (!err && !profile) {
          cache.save_hash(rid, userId, null, null, function(cache_err) {
            // we have no profile, so we have neither skin nor cape
            callback_for(userId, "skin", cache_err, null);
            callback_for(userId, "cape", cache_err, null);
          });
        } else {
          callback_for(userId, type, err, null);
        }
      } else {
        store_skin(rid, userId, profile, details, function(err, skin_hash) {
          cache.save_hash(rid, userId, skin_hash, null, function(cache_err) {
            callback_for(userId, "skin", (err || cache_err), skin_hash);
            store_cape(rid, userId, profile, details, function(err, cape_hash) {
              cache.save_hash(rid, userId, skin_hash, cape_hash, function(cache_err) {
                callback_for(userId, "cape", (err || cache_err), cape_hash);
              });
            });
          });
        });
      }
    });
  } else {
    logging.log(rid + "ID already being processed, adding to queue");
    currently_running.push(new_hash);
  }
}

var exp = {};

// returns true if the +userId+ is a valid userId or username
// the userId may be not exist, however
exp.id_valid = function(userId) {
  return valid_user_id.test(userId);
};

// decides whether to get an image from disk or to download it
// callback contains error, status, hash
// the status gives information about how the image was received
//  -1: "error"
//   0: "none" - cached as null
//   1: "cached" - found on disk
//   2: "downloaded" - profile downloaded, skin downloaded from mojang servers
//   3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
exp.get_image_hash = function(rid, userId, type, callback) {
  cache.get_details(userId, function(err, details) {
    var hash = details !== null ? (type === "skin" ? details.skin : details.cape) : null;
    if (err) {
      callback(err, -1, null);
    } else {
      if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
        // use cached image
        logging.log(rid + "userId cached & recently updated");
        callback(null, (hash ? 1 : 0), hash);
      } else {
        // download image
        if (details) {
          logging.log(rid + "userId cached, but too old");
        } else {
          logging.log(rid + "userId not cached");
        }
        store_images(rid, userId, details, type, function(err, new_hash) {
          if (err) {
            callback(err, -1, details && hash);
          } else {
            var status = details && (hash === new_hash) ? 3 : 2;
            logging.debug(rid + "old hash: " + (details && hash));
            logging.log(rid + "new hash: " + new_hash);
            callback(null, status, new_hash);
          }
        });
      }
    }
  });
};

// handles requests for +userId+ avatars with +size+
// callback contains error, status, image buffer, skin hash
// image is the user's face+helm when helm is true, or the face otherwise
// for status, see get_image_hash
exp.get_avatar = function(rid, userId, helm, size, callback) {
  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
    if (skin_hash) {
      var facepath = __dirname + "/../" + config.faces_dir + skin_hash + ".png";
      var helmpath = __dirname + "/../" + config.helms_dir + skin_hash + ".png";
      var filepath = facepath;
      fs.exists(helmpath, function(exists) {
        if (helm && exists) {
          filepath = helmpath;
        }
        skins.resize_img(filepath, size, function(img_err, result) {
          if (img_err) {
            callback(img_err, -1, null, skin_hash);
          } else {
            // we might have a hash although an error occured
            // (e.g. Mojang servers not reachable, using outdated hash)
            callback(err, (err ? -1 : status), result, skin_hash);
          }
        });
      });
    } else {
      // hash is null when userId has no skin
      callback(err, status, null, null);
    }
  });
};

// handles requests for +userId+ skins
// callback contains error, skin hash, image buffer
exp.get_skin = function(rid, userId, callback) {
  logging.log(rid + "skin request");
  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
    var skinpath = __dirname + "/../" + config.skins_dir + skin_hash + ".png";
    fs.exists(skinpath, function(exists) {
      if (exists) {
        logging.log(rid + "skin already exists, not downloading");
        skins.open_skin(rid, skinpath, function(err, img) {
          callback(err, skin_hash, img);
        });
      } else {
        networking.save_texture(rid, skin_hash, skinpath, function(err, response, img) {
          callback(err, skin_hash, img);
        });
      }
    });
  });
};

function get_type(helm, body) {
  var text = body ? "body" : "head";
  return helm ? text + "helm" : text;
}

// handles creations of 3D renders
// callback contains error, skin hash, image buffer
exp.get_render = function(rid, userId, scale, helm, body, callback) {
  exp.get_skin(rid, userId, function(err, skin_hash, img) {
    if (!skin_hash) {
      callback(err, -1, skin_hash, null);
      return;
    }
    var renderpath = __dirname + "/../" + config.renders_dir + skin_hash + "-" + scale + "-" + get_type(helm, body) + ".png";
    fs.exists(renderpath, function(exists) {
      if (exists) {
        renders.open_render(rid, renderpath, function(err, img) {
          callback(err, 1, skin_hash, img);
        });
        return;
      } else {
        if (!img) {
          callback(err, 0, skin_hash, null);
          return;
        }
        renders.draw_model(rid, img, scale, helm, body, function(err, img) {
          if (err) {
            callback(err, -1, skin_hash, null);
          } else if (!img) {
            callback(null, 0, skin_hash, null);
          } else {
            fs.writeFile(renderpath, img, "binary", function(err) {
              if (err) {
                logging.log(rid + err.stack);
              }
              callback(null, 2, skin_hash, img);
            });
          }
        });
      }
    });
  });
};

// handles requests for +userId+ capes
// callback contains error, cape hash, image buffer
exp.get_cape = function(rid, userId, callback) {
  logging.log(rid + "cape request");
  exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash) {
    if (!cape_hash) {
      callback(err, null, null);
      return;
    }
    var capepath = __dirname + "/../" + config.capes_dir + cape_hash + ".png";
    fs.exists(capepath, function(exists) {
      if (exists) {
        logging.log(rid + "cape already exists, not downloading");
        skins.open_skin(rid, capepath, function(err, img) {
          callback(err, cape_hash, img);
        });
      } else {
        networking.save_texture(rid, cape_hash, capepath, function(err, response, img) {
          if (response && response.statusCode === 404) {
            callback(err, cape_hash, null);
          } else {
            callback(err, cape_hash, img);
          }
        });
      }
    });
  });
};

module.exports = exp;
<commit_msg>move comment to proper position<commit_after>var networking = require("./networking");
var logging = require("./logging");
var config = require("./config");
var cache = require("./cache");
var skins = require("./skins");
var renders = require("./renders");
var fs = require("fs");

// 0098cb60-fa8e-427c-b299-793cbd302c9a
var valid_user_id = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username
var hash_pattern = /[0-9a-f]+$/;

// gets the hash from the textures.minecraft.net +url+
function get_hash(url) {
  return hash_pattern.exec(url)[0].toLowerCase();
}

function store_skin(rid, userId, profile, details, callback) {
  networking.get_skin_url(rid, userId, profile, function(url) {
    if (url) {
      var skin_hash = get_hash(url);
      if (details && details.skin === skin_hash) {
        cache.update_timestamp(rid, userId, skin_hash, function(err) {
          callback(err, skin_hash);
        });
      } else {
        logging.log(rid + "new skin hash: " + skin_hash);
        var facepath = __dirname + "/../" + config.faces_dir + skin_hash + ".png";
        var helmpath = __dirname + "/../" + config.helms_dir + skin_hash + ".png";
        fs.exists(facepath, function(exists) {
          if (exists) {
            logging.log(rid + "skin already exists, not downloading");
            callback(null, skin_hash);
          } else {
            networking.get_from(rid, url, function(img, response, err) {
              if (err || !img) {
                callback(err, null);
              } else {
                skins.extract_face(img, facepath, function(err) {
                  if (err) {
                    logging.error(rid + err.stack);
                    callback(err, null);
                  } else {
                    logging.log(rid + "face extracted");
                    skins.extract_helm(rid, facepath, img, helmpath, function(err) {
                      logging.log(rid + "helm extracted");
                      logging.debug(rid + helmpath);
                      callback(err, skin_hash);
                    });
                  }
                });
              }
            });
          }
        });
      }
    } else {
      callback(null, null);
    }
  });
}

function store_cape(rid, userId, profile, details, callback) {
  networking.get_cape_url(rid, userId, profile, function(url) {
    if (url) {
      var cape_hash = get_hash(url);
      if (details && details.cape === cape_hash) {
        cache.update_timestamp(rid, userId, cape_hash, function(err) {
          callback(err, cape_hash);
        });
      } else {
        logging.log(rid + "new cape hash: " + cape_hash);
        var capepath = __dirname + "/../" + config.capes_dir + cape_hash + ".png";
        fs.exists(capepath, function(exists) {
          if (exists) {
            logging.log(rid + "cape already exists, not downloading");
            callback(null, cape_hash);
          } else {
            networking.get_from(rid, url, function(img, response, err) {
              if (err || !img) {
                logging.error(rid + err.stack);
                callback(err, null);
              } else {
                skins.save_image(img, capepath, function(err) {
                  logging.log(rid + "cape saved");
                  callback(err, cape_hash);
                });
              }
            });
          }
        });
      }
    } else {
      callback(null, null);
    }
  });
}

// used by store_images to queue simultaneous requests for identical userId
// the first request has to be completed until all others are continued
var currently_running = [];
// calls back all queued requests that match userId and type
function callback_for(userId, type, err, hash) {
  var req_count = 0;
  for (var i = 0; i < currently_running.length; i++) {
    var current = currently_running[i];
    if (current.userid === userId && current.type === type) {
      req_count++;
      if (req_count !== 1) {
        // otherwise this would show up on single/first requests, too
        logging.debug(current.rid + "queued " + type + " request continued");
      }
      currently_running.splice(i, 1); // remove from array
      current.callback(err, hash);
      i--;
    }
  }
  if (req_count > 1) {
    logging.debug(req_count + " simultaneous requests for " + userId);
  }
}

// returns true if any object in +arr+ has +value+ as +property+
function deep_property_check(arr, property, value) {
  for (var i = 0; i < arr.length; i++) {
    if (arr[i][property] === value) {
      return true;
    }
  }
  return false;
}

// downloads the images for +userId+ while checking the cache
// status based on +details+. +type+ specifies which
// image type should be called back on
// +callback+ contains error, image hash
function store_images(rid, userId, details, type, callback) {
  var is_uuid = userId.length > 16;
  var new_hash = {
    rid: rid,
    userid: userId,
    type: type,
    callback: callback
  };
  if (!deep_property_check(currently_running, "userid", userId)) {
    currently_running.push(new_hash);
    networking.get_profile(rid, (is_uuid ? userId : null), function(err, profile) {
      if (err || (is_uuid && !profile)) {
        if (!err && !profile) {
          cache.save_hash(rid, userId, null, null, function(cache_err) {
            // we have no profile, so we have neither skin nor cape
            callback_for(userId, "skin", cache_err, null);
            callback_for(userId, "cape", cache_err, null);
          });
        } else {
          callback_for(userId, type, err, null);
        }
      } else {
        store_skin(rid, userId, profile, details, function(err, skin_hash) {
          cache.save_hash(rid, userId, skin_hash, null, function(cache_err) {
            callback_for(userId, "skin", (err || cache_err), skin_hash);
            store_cape(rid, userId, profile, details, function(err, cape_hash) {
              cache.save_hash(rid, userId, skin_hash, cape_hash, function(cache_err) {
                callback_for(userId, "cape", (err || cache_err), cape_hash);
              });
            });
          });
        });
      }
    });
  } else {
    logging.log(rid + "ID already being processed, adding to queue");
    currently_running.push(new_hash);
  }
}

var exp = {};

// returns true if the +userId+ is a valid userId or username
// the userId may be not exist, however
exp.id_valid = function(userId) {
  return valid_user_id.test(userId);
};

// decides whether to get an image from disk or to download it
// callback contains error, status, hash
// the status gives information about how the image was received
//  -1: "error"
//   0: "none" - cached as null
//   1: "cached" - found on disk
//   2: "downloaded" - profile downloaded, skin downloaded from mojang servers
//   3: "checked" - profile re-downloaded (was too old), but it has either not changed or has no skin
exp.get_image_hash = function(rid, userId, type, callback) {
  cache.get_details(userId, function(err, details) {
    var hash = details !== null ? (type === "skin" ? details.skin : details.cape) : null;
    if (err) {
      callback(err, -1, null);
    } else {
      if (details && details.time + config.local_cache_time * 1000 >= new Date().getTime()) {
        // use cached image
        logging.log(rid + "userId cached & recently updated");
        callback(null, (hash ? 1 : 0), hash);
      } else {
        // download image
        if (details) {
          logging.log(rid + "userId cached, but too old");
        } else {
          logging.log(rid + "userId not cached");
        }
        store_images(rid, userId, details, type, function(err, new_hash) {
          if (err) {
            // we might have a hash although an error occured
            // (e.g. Mojang servers not reachable, using outdated hash)
            callback(err, -1, details && hash);
          } else {
            var status = details && (hash === new_hash) ? 3 : 2;
            logging.debug(rid + "old hash: " + (details && hash));
            logging.log(rid + "new hash: " + new_hash);
            callback(null, status, new_hash);
          }
        });
      }
    }
  });
};

// handles requests for +userId+ avatars with +size+
// callback contains error, status, image buffer, skin hash
// image is the user's face+helm when helm is true, or the face otherwise
// for status, see get_image_hash
exp.get_avatar = function(rid, userId, helm, size, callback) {
  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
    if (skin_hash) {
      var facepath = __dirname + "/../" + config.faces_dir + skin_hash + ".png";
      var helmpath = __dirname + "/../" + config.helms_dir + skin_hash + ".png";
      var filepath = facepath;
      fs.exists(helmpath, function(exists) {
        if (helm && exists) {
          filepath = helmpath;
        }
        skins.resize_img(filepath, size, function(img_err, result) {
          if (img_err) {
            callback(img_err, -1, null, skin_hash);
          } else {
            callback(err, (err ? -1 : status), result, skin_hash);
          }
        });
      });
    } else {
      // hash is null when userId has no skin
      callback(err, status, null, null);
    }
  });
};

// handles requests for +userId+ skins
// callback contains error, skin hash, image buffer
exp.get_skin = function(rid, userId, callback) {
  logging.log(rid + "skin request");
  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
    var skinpath = __dirname + "/../" + config.skins_dir + skin_hash + ".png";
    fs.exists(skinpath, function(exists) {
      if (exists) {
        logging.log(rid + "skin already exists, not downloading");
        skins.open_skin(rid, skinpath, function(err, img) {
          callback(err, skin_hash, img);
        });
      } else {
        networking.save_texture(rid, skin_hash, skinpath, function(err, response, img) {
          callback(err, skin_hash, img);
        });
      }
    });
  });
};

function get_type(helm, body) {
  var text = body ? "body" : "head";
  return helm ? text + "helm" : text;
}

// handles creations of 3D renders
// callback contains error, skin hash, image buffer
exp.get_render = function(rid, userId, scale, helm, body, callback) {
  exp.get_skin(rid, userId, function(err, skin_hash, img) {
    if (!skin_hash) {
      callback(err, -1, skin_hash, null);
      return;
    }
    var renderpath = __dirname + "/../" + config.renders_dir + skin_hash + "-" + scale + "-" + get_type(helm, body) + ".png";
    fs.exists(renderpath, function(exists) {
      if (exists) {
        renders.open_render(rid, renderpath, function(err, img) {
          callback(err, 1, skin_hash, img);
        });
        return;
      } else {
        if (!img) {
          callback(err, 0, skin_hash, null);
          return;
        }
        renders.draw_model(rid, img, scale, helm, body, function(err, img) {
          if (err) {
            callback(err, -1, skin_hash, null);
          } else if (!img) {
            callback(null, 0, skin_hash, null);
          } else {
            fs.writeFile(renderpath, img, "binary", function(err) {
              if (err) {
                logging.log(rid + err.stack);
              }
              callback(null, 2, skin_hash, img);
            });
          }
        });
      }
    });
  });
};

// handles requests for +userId+ capes
// callback contains error, cape hash, image buffer
exp.get_cape = function(rid, userId, callback) {
  logging.log(rid + "cape request");
  exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash) {
    if (!cape_hash) {
      callback(err, null, null);
      return;
    }
    var capepath = __dirname + "/../" + config.capes_dir + cape_hash + ".png";
    fs.exists(capepath, function(exists) {
      if (exists) {
        logging.log(rid + "cape already exists, not downloading");
        skins.open_skin(rid, capepath, function(err, img) {
          callback(err, cape_hash, img);
        });
      } else {
        networking.save_texture(rid, cape_hash, capepath, function(err, response, img) {
          if (response && response.statusCode === 404) {
            callback(err, cape_hash, null);
          } else {
            callback(err, cape_hash, img);
          }
        });
      }
    });
  });
};

module.exports = exp;
<|endoftext|>


<commit_before>/*
 * globule
 * https://github.com/cowboy/node-globule
 *
 * Copyright (c) 2014 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 */

'use strict';

var fs = require('fs');
var path = require('path');

var _ = require('lodash');
var glob = require('glob');
var minimatch = require('minimatch');

// The module.
var globule = exports;

// Process specified wildcard glob patterns or filenames against a
// callback, excluding and uniquing files in the result set.
function processPatterns(patterns, options, fn) {
  var result = [];
  _.each(patterns, function(pattern) {
    // The first character is not ! (inclusion). Add all matching filepaths
    // to the result set.
    if (pattern.indexOf('!') !== 0) {
      result = _.union(result, fn(pattern));
      return;
    }
    // The first character is ! (exclusion). Remove any filepaths from the
    // result set that match this pattern, sans leading !.
    var filterFn = minimatch.filter(pattern.slice(1), options);
    result = _.filter(result, function(filepath) {
      return !filterFn(filepath);
    });
  });
  return result;
}

// Match a filepath or filepaths against one or more wildcard patterns. Returns
// all matching filepaths. This behaves just like minimatch.match, but supports
// any number of patterns.
globule.match = function(patterns, filepaths, options) {
  // Return empty set if either patterns or filepaths was omitted.
  if (patterns == null || filepaths == null) { return []; }
  // Normalize patterns and filepaths to flattened arrays.
  patterns = _.isArray(patterns) ? _.flattenDeep(patterns) : [patterns];
  filepaths = _.isArray(filepaths) ? _.flattenDeep(filepaths) : [filepaths];
  // Return empty set if there are no patterns or filepaths.
  if (patterns.length === 0 || filepaths.length === 0) { return []; }
  // Return all matching filepaths.
  return processPatterns(patterns, options, function(pattern) {
    return minimatch.match(filepaths, pattern, options || {});
  });
};

// Match a filepath or filepaths against one or more wildcard patterns. Returns
// true if any of the patterns match.
globule.isMatch = function() {
  return globule.match.apply(null, arguments).length > 0;
};

// Return an array of all file paths that match the given wildcard patterns.
globule.find = function() {
  var args = _.toArray(arguments);
  // If the last argument is an options object, remove it from args.
  var options = _.isPlainObject(args[args.length - 1]) ? args.pop() : {};
  // If options.src was specified, use it. Otherwise, use all non-options
  // arguments. Flatten nested arrays.
  var patterns;
  if (options.src) {
    patterns = _.isArray(options.src) ? _.flattenDeep(options.src) : [options.src];
  } else {
    patterns = _.flattenDeep(args);
  }
  // Return empty set if there are no patterns.
  if (patterns.length === 0) { return []; }
  var srcBase = options.srcBase || options.cwd;
  // Create glob-specific options object.
  var globOptions = _.extend({}, options);
  if (srcBase) {
    globOptions.cwd = srcBase;
  }
  // Get all matching filepaths.
  var matches = processPatterns(patterns, options, function(pattern) {
    return glob.sync(pattern, globOptions);
  });
  // If srcBase and prefixBase were specified, prefix srcBase to matched paths.
  if (srcBase && options.prefixBase) {
    matches = matches.map(function(filepath) {
      return path.join(srcBase, filepath);
    });
  }
  // Filter result set?
  if (options.filter) {
    matches = matches.filter(function(filepath) {
      // If srcBase was specified but prefixBase was NOT, prefix srcBase
      // temporarily, for filtering.
      if (srcBase && !options.prefixBase) {
        filepath = path.join(srcBase, filepath);
      }
      try {
        if (_.isFunction(options.filter)) {
          return options.filter(filepath, options);
        } else {
          // If the file is of the right type and exists, this should work.
          return fs.statSync(filepath)[options.filter]();
        }
      } catch(err) {
        // Otherwise, it's probably not the right type.
        return false;
      }
    });
  }
  return matches;
};

var pathSeparatorRe = /[\/\\]/g;
var extDotRe = {
  first: /(\.[^\/]*)?$/,
  last: /(\.[^\/\.]*)?$/,
};
function rename(dest, options) {
  // Flatten path?
  if (options.flatten) {
    dest = path.basename(dest);
  }
  // Change the extension?
  if (options.ext) {
    dest = dest.replace(extDotRe[options.extDot], options.ext);
  }
  // Join dest and destBase?
  if (options.destBase) {
    dest = path.join(options.destBase, dest);
  }
  return dest;
}

// Build a mapping of src-dest filepaths from the given set of filepaths.
globule.mapping = function(filepaths, options) {
  // Return empty set if filepaths was omitted.
  if (filepaths == null) { return []; }
  options = _.defaults({}, options, {
    extDot: 'first',
    rename: rename,
  });
  var files = [];
  var fileByDest = {};
  // Find all files matching pattern, using passed-in options.
  filepaths.forEach(function(src) {
    // Generate destination filename.
    var dest = options.rename(src, options);
    // Prepend srcBase to all src paths.
    if (options.srcBase) {
      src = path.join(options.srcBase, src);
    }
    // Normalize filepaths to be unix-style.
    dest = dest.replace(pathSeparatorRe, '/');
    src = src.replace(pathSeparatorRe, '/');
    // Map correct src path to dest path.
    if (fileByDest[dest]) {
      // If dest already exists, push this src onto that dest's src array.
      fileByDest[dest].src.push(src);
    } else {
      // Otherwise create a new src-dest file mapping object.
      files.push({
        src: [src],
        dest: dest,
      });
      // And store a reference for later use.
      fileByDest[dest] = files[files.length - 1];
    }
  });
  return files;
};

// Return a mapping of src-dest filepaths from files matching the given
// wildcard patterns.
globule.findMapping = function() {
  var args = _.toArray(arguments);
  // If the last argument is an options object, remove it from args.
  var options = _.isPlainObject(args[args.length - 1]) ? args.pop() : {};
  // Generate mapping from found filepaths.
  return globule.mapping(globule.find(args, options), options);
};
<commit_msg>Return unix-style paths on Windows when prefixBase is on<commit_after>/*
 * globule
 * https://github.com/cowboy/node-globule
 *
 * Copyright (c) 2014 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 */

'use strict';

var fs = require('fs');
var path = require('path');

var _ = require('lodash');
var glob = require('glob');
var minimatch = require('minimatch');

// The module.
var globule = exports;

// Process specified wildcard glob patterns or filenames against a
// callback, excluding and uniquing files in the result set.
function processPatterns(patterns, options, fn) {
  var result = [];
  _.each(patterns, function(pattern) {
    // The first character is not ! (inclusion). Add all matching filepaths
    // to the result set.
    if (pattern.indexOf('!') !== 0) {
      result = _.union(result, fn(pattern));
      return;
    }
    // The first character is ! (exclusion). Remove any filepaths from the
    // result set that match this pattern, sans leading !.
    var filterFn = minimatch.filter(pattern.slice(1), options);
    result = _.filter(result, function(filepath) {
      return !filterFn(filepath);
    });
  });
  return result;
}

// Normalize paths to be unix-style.
var pathSeparatorRe = /[\/\\]/g;
function normalizePath(path) {
  return path.replace(pathSeparatorRe, '/');
}

// Match a filepath or filepaths against one or more wildcard patterns. Returns
// all matching filepaths. This behaves just like minimatch.match, but supports
// any number of patterns.
globule.match = function(patterns, filepaths, options) {
  // Return empty set if either patterns or filepaths was omitted.
  if (patterns == null || filepaths == null) { return []; }
  // Normalize patterns and filepaths to flattened arrays.
  patterns = _.isArray(patterns) ? _.flattenDeep(patterns) : [patterns];
  filepaths = _.isArray(filepaths) ? _.flattenDeep(filepaths) : [filepaths];
  // Return empty set if there are no patterns or filepaths.
  if (patterns.length === 0 || filepaths.length === 0) { return []; }
  // Return all matching filepaths.
  return processPatterns(patterns, options, function(pattern) {
    return minimatch.match(filepaths, pattern, options || {});
  });
};

// Match a filepath or filepaths against one or more wildcard patterns. Returns
// true if any of the patterns match.
globule.isMatch = function() {
  return globule.match.apply(null, arguments).length > 0;
};

// Return an array of all file paths that match the given wildcard patterns.
globule.find = function() {
  var args = _.toArray(arguments);
  // If the last argument is an options object, remove it from args.
  var options = _.isPlainObject(args[args.length - 1]) ? args.pop() : {};
  // If options.src was specified, use it. Otherwise, use all non-options
  // arguments. Flatten nested arrays.
  var patterns;
  if (options.src) {
    patterns = _.isArray(options.src) ? _.flattenDeep(options.src) : [options.src];
  } else {
    patterns = _.flattenDeep(args);
  }
  // Return empty set if there are no patterns.
  if (patterns.length === 0) { return []; }
  var srcBase = options.srcBase || options.cwd;
  // Create glob-specific options object.
  var globOptions = _.extend({}, options);
  if (srcBase) {
    globOptions.cwd = srcBase;
  }
  // Get all matching filepaths.
  var matches = processPatterns(patterns, options, function(pattern) {
    return glob.sync(pattern, globOptions);
  });
  // If srcBase and prefixBase were specified, prefix srcBase to matched paths.
  if (srcBase && options.prefixBase) {
    matches = matches.map(function(filepath) {
      return normalizePath(path.join(srcBase, filepath));
    });
  }
  // Filter result set?
  if (options.filter) {
    matches = matches.filter(function(filepath) {
      // If srcBase was specified but prefixBase was NOT, prefix srcBase
      // temporarily, for filtering.
      if (srcBase && !options.prefixBase) {
        filepath = normalizePath(path.join(srcBase, filepath));
      }
      try {
        if (_.isFunction(options.filter)) {
          return options.filter(filepath, options);
        } else {
          // If the file is of the right type and exists, this should work.
          return fs.statSync(filepath)[options.filter]();
        }
      } catch(err) {
        // Otherwise, it's probably not the right type.
        return false;
      }
    });
  }
  return matches;
};

var extDotRe = {
  first: /(\.[^\/]*)?$/,
  last: /(\.[^\/\.]*)?$/,
};
function rename(dest, options) {
  // Flatten path?
  if (options.flatten) {
    dest = path.basename(dest);
  }
  // Change the extension?
  if (options.ext) {
    dest = dest.replace(extDotRe[options.extDot], options.ext);
  }
  // Join dest and destBase?
  if (options.destBase) {
    dest = path.join(options.destBase, dest);
  }
  return dest;
}

// Build a mapping of src-dest filepaths from the given set of filepaths.
globule.mapping = function(filepaths, options) {
  // Return empty set if filepaths was omitted.
  if (filepaths == null) { return []; }
  options = _.defaults({}, options, {
    extDot: 'first',
    rename: rename,
  });
  var files = [];
  var fileByDest = {};
  // Find all files matching pattern, using passed-in options.
  filepaths.forEach(function(src) {
    // Generate destination filename.
    var dest = options.rename(src, options);
    // Prepend srcBase to all src paths.
    if (options.srcBase) {
      src = path.join(options.srcBase, src);
    }
    // Normalize filepaths to be unix-style.
    dest = normalizePath(dest);
    src = normalizePath(src);
    // Map correct src path to dest path.
    if (fileByDest[dest]) {
      // If dest already exists, push this src onto that dest's src array.
      fileByDest[dest].src.push(src);
    } else {
      // Otherwise create a new src-dest file mapping object.
      files.push({
        src: [src],
        dest: dest,
      });
      // And store a reference for later use.
      fileByDest[dest] = files[files.length - 1];
    }
  });
  return files;
};

// Return a mapping of src-dest filepaths from files matching the given
// wildcard patterns.
globule.findMapping = function() {
  var args = _.toArray(arguments);
  // If the last argument is an options object, remove it from args.
  var options = _.isPlainObject(args[args.length - 1]) ? args.pop() : {};
  // Generate mapping from found filepaths.
  return globule.mapping(globule.find(args, options), options);
};
<|endoftext|>


<commit_before>var express = require('express'),
    _ = require('underscore')._,
    fs = require('fs');

var webInterface = function(dbot) {
    var pub = 'public';
    var app = express();

    app.use(express.static(pub));
    app.set('view engine', 'jade');

    app.get('/', function(req, res) {
        res.render('index', { 'name': dbot.config.name });
    });

    console.log(Object.keys(app));

    app.configure(function(){
        app.use(function(err, req, res, next) {
            console.log('Web encountered an error: ' + err);
         });
    });
    app.on('error', function(err) {
        console.log('Web encountered an error: ' + err);
    });

    var server = app.listen(dbot.config.web.webPort);

    this.reloadPages = function() {
        var pages = dbot.pages;
        for(var p in pages) {
            if(_.has(pages, p)) {
                var func = pages[p];
                var mod = func.module;
                app.get(p, (function(req, resp) {
                    // Crazy shim to seperate module views.
                    var shim = Object.create(resp);
                    shim.render = (function(view, one, two) {
                        // Render with express.js
                        resp.render(this.module + '/' + view, one, two);
                    }).bind(this);
                    shim.render_core = resp.render;
                    this.call(this.module, req, shim);
                }).bind(func));
            }
        }
    }.bind(this);

    this.onDestroy = function() {
        server.close();
    }
};

exports.fetch = function(dbot) {
    return new webInterface(dbot);
};
<commit_msg>revert web shit<commit_after>var express = require('express'),
    _ = require('underscore')._,
    fs = require('fs');

var webInterface = function(dbot) {
    var pub = 'public';
    var app = express();

    app.use(express.static(pub));
    app.set('view engine', 'jade');

    app.get('/', function(req, res) {
        res.render('index', { 'name': dbot.config.name });
    });

    var server = app.listen(dbot.config.web.webPort);

    this.reloadPages = function() {
        var pages = dbot.pages;
        for(var p in pages) {
            if(_.has(pages, p)) {
                var func = pages[p];
                var mod = func.module;
                app.get(p, (function(req, resp) {
                    // Crazy shim to seperate module views.
                    var shim = Object.create(resp);
                    shim.render = (function(view, one, two) {
                        // Render with express.js
                        resp.render(this.module + '/' + view, one, two);
                    }).bind(this);
                    shim.render_core = resp.render;
                    this.call(this.module, req, shim);
                }).bind(func));
            }
        }
    }.bind(this);

    this.onDestroy = function() {
        server.close();
    }
};

exports.fetch = function(dbot) {
    return new webInterface(dbot);
};
<|endoftext|>


<commit_before>var fs = require('fs');
var path = require('path');
var parser = require('./parser');

var webstack = [];

exports = module.exports = handler = {};

handler.webstack = webstack;

handler.handle = function(req, res,getbody) {
	var layercount = -1;
	var pathurl = parser.parsepath(req.url).path;
	var method = req.method;
	var fileStream;
	var body='';
	req.query = parser.parsepath(req.url).query;
	if (getbody) {
		req.setEncoding = 'utf8';
		req.on('data', function(chunk) {
			body += chunk;
		})
		req.on('end', function() {
			req.body=parser.parse(body,req.headers['content-type']);
			console.log(req.body);
		});
	}
	return function cb(err) {++layercount;
		if (err) {
			throw new Error(err);
		}
		if (layercount < webstack.length) {
			switch(webstack[layercount].type) {
			case 'middleware':
				webstack[layercount].callback(req, res, cb);
				break;
			case 'static':
				var filename = path.join(webstack[layercount].dir, req.url);
				fs.exists(filename, function(exists) {
					if (exists) {
						if (fs.statSync(filename).isFile()) {
							fileStream = fs.createReadStream(filename);
							fileStream.pipe(res);
							fileStream.on('error', cb);
						} else {
							cb();
						}
					} else {
						cb();
					}
				});
				break;
			case 'api':
				if (webstack[layercount].path === pathurl && webstack[layercount].method === method) {
					webstack[layercount].callback(req, res);
				} else if (webstack[layercount].method === method) {
					if (parser.parseParams(req, webstack[layercount].path, pathurl)) {
						webstack[layercount].callback(req, res);
					} else {
						cb();
					}
				} else {
					cb();
				}
				break;
			case 'default':
				cb()
			}
		} else {
			res.end("can't get the route " + req.url);
		}
	}
};
<commit_msg>add:body parsing<commit_after>var fs = require('fs');
var path = require('path');
var parser = require('./parser');

var webstack = [];

exports = module.exports = handler = {};

handler.webstack = webstack;

handler.handle = function(req, res,getbody) {
	var layercount = -1;
	var pathurl = parser.parsepath(req.url).path;
	var method = req.method;
	var fileStream;
	var body='';
	req.query = parser.parsepath(req.url).query;
	if (getbody) {
		req.setEncoding = 'utf8';
		req.on('data', function(chunk) {
			body += chunk;
		})
		req.on('end', function() {
			req.body=parser.parse(body,req.headers['content-type']);
		});
	}
	return function cb(err) {++layercount;
		if (err) {
			throw new Error(err);
		}
		if (layercount < webstack.length) {
			switch(webstack[layercount].type) {
			case 'middleware':
				webstack[layercount].callback(req, res, cb);
				break;
			case 'static':
				var filename = path.join(webstack[layercount].dir, req.url);
				fs.exists(filename, function(exists) {
					if (exists) {
						if (fs.statSync(filename).isFile()) {
							fileStream = fs.createReadStream(filename);
							fileStream.pipe(res);
							fileStream.on('error', cb);
						} else {
							cb();
						}
					} else {
						cb();
					}
				});
				break;
			case 'api':
				if (webstack[layercount].path === pathurl && webstack[layercount].method === method) {
					webstack[layercount].callback(req, res);
				} else if (webstack[layercount].method === method) {
					if (parser.parseParams(req, webstack[layercount].path, pathurl)) {
						webstack[layercount].callback(req, res);
					} else {
						cb();
					}
				} else {
					cb();
				}
				break;
			case 'default':
				cb()
			}
		} else {
			res.end("can't get the route " + req.url);
		}
	}
};
<|endoftext|>


<commit_before>var http = require('http');
var agent = new (http.Agent)({maxSockets: 8});
var tls = require('tls');
var CRLF = '\r\n';
var IncomingMessage = require('./http-incoming');

function getHeadersContent(req) {
	var headers = [req.method + ' ' + req.url + ' HTTP/' + req.httpVersion];
	if (req.headers && !req.headers.Host) {
		req.headers.Host = req.headers.host;
	}
	for (var key in req.headers) {
		var line = req.headers[key];
		if (Array.isArray(line)) {
			line.forEach(function(value) {
				headers.push(key + ': ' + value);
			});
		} else {
			headers.push(key + ': ' + line);
		}
	}
	headers.push(CRLF);

	return headers.join(CRLF);
}

module.exports = function proxy(req, callback) {
	var headers = req.headers;
	var options = req.options;
	var done, proxyReq;

	function execCallback(err, proxyRes) {
		if (!done) {
			callback && callback(err, proxyRes);
			done = true;
		}
	}

	options.method = 'CONNECT';
	options.path = headers.host;
	if (options.agent == null) {
		options.agent = agent;
	}
	options.headers = {
			host : headers.host,
			'proxy-connection': 'keep-alive',
			'user-agent': headers['user-agent']
		};

	var connect = http.request(options).on('error', execCallback)
	.on('connect', function (res, socket, head) {
		socket.on('error', execCallback);
	    proxyReq = tls.connect({
	        rejectUnauthorized: false,
	        socket: socket
	    }, function () {
	        proxyReq.write(getHeadersContent(req));
	        req.pipe(proxyReq);

	        var proxyRes = new IncomingMessage(proxyReq);
	        proxyRes.on('response', function(res) {
		    	execCallback(null, res);
		    });
	        proxyRes.on('error', execCallback);
	    });
	});
	connect.end();

	req.on('error', function(err) {
		connect.abort();
		proxyReq && proxyReq.destroy();
	});
};

<commit_msg>https-proxy<commit_after>var http = require('http');
var agent = new (http.Agent)({maxSockets: 8});
var tls = require('tls');
var CRLF = '\r\n';
var IncomingMessage = require('./http-incoming');

function getHeadersContent(req) {
	var headers = [req.method + ' ' + req.url + ' HTTP/' + req.httpVersion];
	if (req.headers && !req.headers.Host) {
		req.headers.Host = req.headers.host;
	}
	for (var key in req.headers) {
		var line = req.headers[key];
		if (Array.isArray(line)) {
			line.forEach(function(value) {
				headers.push(key + ': ' + value);
			});
		} else {
			headers.push(key + ': ' + line);
		}
	}
	headers.push(CRLF);

	return headers.join(CRLF);
}

module.exports = function proxy(req, callback) {
	var headers = req.headers;
	var options = req.options;
	var done, proxyReq;

	function execCallback(err, proxyRes) {
		if (!done) {
			callback && callback(err, proxyRes);
			done = true;
		}
	}

	options.method = 'CONNECT';
	options.path = headers.host;
	if (options.agent == null) {
		options.agent = agent;
	}
	options.headers = {
			host : headers.host,
			'proxy-connection': 'keep-alive',
			'user-agent': headers['user-agent']
		};

	var connect = http.request(options).on('error', execCallback)
	.on('connect', function (res, socket, head) {
		socket.on('error', execCallback);
	    proxyReq = tls.connect({
	        rejectUnauthorized: false,
	        socket: socket
	    }, function () {
	    	proxyReq.abort = proxyReq.destroy;
	        proxyReq.write(getHeadersContent(req));
	        req.pipe(proxyReq);

	        var proxyRes = new IncomingMessage(proxyReq);
	        proxyRes.on('response', function(res) {
		    	execCallback(null, res);
		    });
	        proxyRes.on('error', execCallback);
	    });
	});
	connect.end();

	req.on('error', function(err) {
		connect.abort();
		proxyReq && proxyReq.destroy();
	});
};

<|endoftext|>