<commit_before>// This file is part of ghini.web
// http://github.com/Ghini/ghini.web
//
// ghini.web is free software: you can redistribute ghini.web and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or (at
// your option) any later version.
//
// ghini.web is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
// License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with ghini.web.  If not, see <http://www.gnu.org/licenses/>.
//
// this is the remote server

var http = require('http');
http.globalAgent.maxSockets = 10;

var config = require('./config');
var express = require("express");
var app = express();
var port = Number(process.env.PORT || config.port);  // servicing on port
var dburl = process.env.DATABASE_URL || config.database_url;
console.log(dburl);

var mongodb = require('mongodb');
var dbclient = mongodb.MongoClient;
const mongoose = require('mongoose');

String.prototype.formatU = function() {
    var str = this.toString();
    if (!arguments.length)
        return str;
    var args = typeof arguments[0];
    args = (("string" == args || "number" == args) ? arguments : arguments[0]);
    for (var arg in args)
        str = str.replace(RegExp("\\{" + arg + "\\}", "gi"), args[arg]);
    return str;
};

var sizethree = function (i) {
    var result = '000' + (i || 0);
    return (result).substr(result.length-3);
}

app.set('views', __dirname + '/views');
app.set('view engine', "pug");
app.engine('pug', require('pug').__express);
app.use(express.static(__dirname + '/public'));

var favicon = require('serve-favicon');
app.use(favicon(__dirname + '/public/img/favicon.ico'));

app.get("/", function(req, res){
    res.render("map");
    console.log(dburl);
});

app.get("/panels/:garden_id/:language?", (req, res) => {
    var language = req.params.language || "en";
    if (!language.match("^[a-z][a-z]$")) {
        language = "en";
    }
    var connection = mongoose.createConnection(dburl);
    connection.on('connected', () => {
        var schema = new mongoose.Schema({});
        var Panel = connection.model('infopanels', schema);
        var result = [];

        Panel.
            aggregate([{$match: {garden_id: parseInt(req.params.garden_id)}}]).
            cursor().
            exec().
            on('data', (doc) => {
                doc.url = ("http://www.ghini.me/raw/g_{0}_{1}_{2}.mp3").formatU(
                    sizethree(doc.garden_id), language, sizethree(doc.id_within_garden));
                result.push(doc);
            }).
            on('error', (err) => {
                console.log("err:", err);
            }).
            on('end', () => {
                res.render('panel', {doc: result});
            });
    });
});

app.get("/gardens", (req, res) => {
    var connection = mongoose.createConnection(dburl);
    connection.on('connected', () => {
        var schema = new mongoose.Schema({});
        var Garden = connection.model('garden', schema);
        var result = [];

        Garden.
            aggregate([ {$sort: {name:1}},
                        {$lookup: {from:"infopanels", foreignField:"garden", localField:"name", as:"infopanels"}},
                        {$project: {name:1, lat: 1, lon: 1, contact: 1, id: 1,
                                    title: "$name",
                                    infopanels: {$size: "$infopanels"}}} ]).
            cursor().
            exec().
            on('data', (doc) => {result.push(doc)}).
            on('error', (err) => {
                console.log("err:", err);
            }).
            on('end', () => {
                res.render('garden', {doc: result});
            });
    });
});

// make the application listen to the port
// we pass the ExpressJS server to Socket.io. In effect, our real time
// communication will still happen on the same port.
var io = require('socket.io').listen(app.listen(port, function() {
            console.log("Listening on " + port);
        }));

io.sockets.on('connection', function (socket) {
    // Upon a successful connection, we send the list of gardens, with the
    // plants count per garden. gardens are objects as any other object, they
    // have associated the colour red and the icon home, and the zoom at which
    // they appear is 2. then we issue the command 'map-set-view' to the world.

    // As instructed by the final user (perusing garden icons or menu item),
    // the clients issue the 'select-garden' command.  Upon receiving this
    // command from a client, the server issues the command 'map-set-view'
    // (garden position and extent), followed by as many 'add-object'
    // commands as needed to send the lists of objects relative to the
    // selected garden.  These include plants, photos, infopanels.
    dbclient.connect(dburl, function (err, db) {
        if (err) {
            console.log('Unable to connect to the mongoDB server. Error:', err);
        } else {
            // Get the collection of all gardens, each with the amount of plants;
            // We store our find criteria in a cursor.
            // Having a cursor does not mean we performed any database access, yet.
            var cursor = db.collection('gardens').aggregate(
                {$sort:{name:1}},
                {$lookup:{from:"plants", foreignField:"garden", localField:"name", as:"plants"}},
                {$lookup:{from:"photos", foreignField:"garden", localField:"name", as:"photos"}},
                {$lookup:{from:"infopanels", foreignField:"garden", localField:"name", as:"infopanels"}},
                {$project: {layer_name: {$literal: "gardens"},
                            layer_zoom: {$literal: 1},
                            name:1, lat: 1, lon: 1, contact: 1,
                            title: "$name",
                            plants: {$size: "$plants"},
                            photos: {$size: "$photos"},
                            infopanels: {$size: "$infopanels"},
                            draggable: {$literal: false},
                            color: {$literal: "red"},
                            icon: {$literal: "home"}}},
                {});
            // Lets iterate on the result.
            // this will access the database, so we act in a callback.
            cursor.each(function (err, doc) {
                if (err || !doc) {
                    console.log("err:", err, "; doc:", doc);
                } else {
                    socket.emit('add-object', doc);
                }
            });
            cursor = db.collection('plants').aggregate(
                {$sort:{code:1}},
                {$group: {_id: {species:"$species", garden: "$garden"}, count: {$sum: 1}, plants: {$push: {_id: "$_id", code: "$code"}}}},
                {$group: {_id: {species:"$_id.species"},
                          gardens: {$push: {name: "$_id.garden", plant_count: "$count", plants: "$plants"}}}},
                {$lookup: {from:"taxa", localField:"_id.species", foreignField:"name", as:"taxon"}},
                {$unwind: {path: "$taxon"}},
                {$project: {_id: "$taxon._id",
                            phonetic: "$taxon.phonetic",
                            species_name: "$taxon.name",
                            family_name: "$taxon.family",
                            gardens: 1, taxon: 1,
                            layer_name: {$literal: "__taxa"}}},
                {$sort:{family_name:1, species_name:1}},
                {});
            // Lets iterate on the result.
            // this will access the database, so we act in a callback.
            cursor.each(function (err, doc) {
                if (err || !doc) {
                    console.log("err:", err, "; doc:", doc);
                } else {
                    socket.emit('add-object', doc);
                }
            });
        }});

    socket.on('select-garden', function (args) {
        // require that client wipes all data relative to the current garden
        socket.emit('map-remove-objects', 'plants');
        socket.emit('map-remove-objects', 'photos');
        socket.emit('map-remove-objects', 'infopanels');
        var defaults = {
            zoom: 2,
            lat: 32.0,
            lon: 8.0
        };
        if ('map' in args) {
            Object.assign(args, args.map);
        } else {
            Object.assign(args, defaults);
        }
        if (!('garden' in args)) {
            console.log(args);
            socket.emit('map-set-view', args);
            return;
        }
        dbclient.connect(dburl, function (err, db) {
            if (err) {
                console.log('Unable to connect to the mongoDB server. Error:', err);
            } else {
                // first of all, tell the client to set the view on the
                // garden; the garden document contains lat, lon, and zoom.
                var cursor = db.collection('gardens').findOne({name:args['garden']}, function(err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc, "garden not found", args);
                        console.log('map-set-view', args);
                        socket.emit('map-set-view', args);
                    } else {
                        if ('map' in args) {
                            Object.assign(doc, args.map);
                        }
                        console.log('map-set-view', doc);
                        socket.emit('map-set-view', doc);
                    }
                });

                // Get the plants relative to this garden.
                // We store our find criteria in a cursor.
                // Having a cursor does not mean we performed any database access, yet.
                cursor = db.collection('plants').aggregate(
                    {$match:{garden:args['garden']}},
                    {$lookup:{from:"taxa", localField:"species", foreignField:"name", as:"taxon"}},
                    {$unwind: {path: "$taxon"}},
                    {$project: {layer_name: {$literal: "plants"},
                                layer_zoom: "$zoom",
                                lat: 1, lon: 1, species:1, taxon:1, code:1,
                                title: "$code",
                                species_name: "$taxon.name",
                                vernacular: "$taxon.vernacular",
                                phonetic: "$taxon.phonetic",
                                family: "$taxon.family",
                                draggable: {$literal: false},
                                color: {$literal: "green"},
                                icon: {$literal: "tree-evergreen"}}},
                    {}
                );
                // Lets iterate on the result.
                // this will access the database, so we act in a callback.
                cursor.each(function (err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc);
                    } else {
                        switch(doc.family){
                        case 'Arecaceae':
                            doc.icon = 'palm-tree';
                            break;
                        case 'Musaceae':
                            doc.icon = 'banana-tree';
                            break;
                        case 'Araucariaceae':
                        case 'Cupressaceae':
                        case 'Cycadaceae':
                        case 'Ephedraceae':
                        case 'Ginkgoaceae':
                        case 'Gnetaceae':
                        case 'Pinaceae':
                        case 'Podocarpaceae':
                        case 'Sciadopityaceae':
                        case 'Taxaceae':
                        case 'Welwitschiaceae':
                        case 'Zamiaceae ':
                            doc.icon = 'tree-conifer';
                            break;
                        case 'Anemiaceae':
                        case 'Apleniaceae':
                        case 'Aspleniaceae':
                        case 'Athyriaceae':
                        case 'Blechnaceae':
                        case 'Cibotiaceae':
                        case 'Culcitaceae':
                        case 'Cyatheaceae':
                        case 'Cystodiaceae':
                        case 'Cystopteridaceae':
                        case 'Davalliaceae':
                        case 'Dennstaedtiaceae':
                        case 'Dicksoniaceae':
                        case 'Diplaziopsidaceae':
                        case 'Dipteridaceae':
                        case 'Dryopteridacae':
                        case 'Dryopteridaceae':
                        case 'Equisetaceae':
                        case 'Gleicheniaceae':
                        case 'Hymenophyllaceae':
                        case 'Hypodematiaceae':
                        case 'Isoëtaceae':
                        case 'Lindsaeaceae':
                        case 'Lomariopsidaceae':
                        case 'Lonchitidaceae':
                        case 'Loxsomataceae':
                        case 'Lycopodiaceae':
                        case 'Lygodiaceae':
                        case 'Marattiaceae':
                        case 'Marsileaceae':
                        case 'Matoniaceae':
                        case 'Metaxyaceae':
                        case 'Nephrolepidaceae':
                        case 'Oleandraceae':
                        case 'Onocleaceae':
                        case 'Ophioglossaceae':
                        case 'Osmundaceae':
                        case 'Plagiogyriaceae':
                        case 'Polypodiaceae':
                        case 'Psilotaceae':
                        case 'Pteridaceae':
                        case 'Rhachidosoraceae':
                        case 'Saccolomataceae':
                        case 'Salviniaceae':
                        case 'Schizaeaceae':
                        case 'Selaginellaceae':
                        case 'Tectariaceae':
                        case 'Thelypteridaceae':
                        case 'Thyrsopteridaceae':
                        case 'Woodsiaceae':
                            // doc.icon = 'fern'; // there is no such icon yet
                            break;
                        case 'Asteraceae':
                            doc.icon = 'compositae';
                            break;
                        }
                        socket.emit('add-object', doc);
                    }
                });

                // same for the photos
                cursor = db.collection('photos').aggregate(
                    {$match:{garden:args['garden']}},
                    {$project: {layer_name: {$literal: "photos"},
                                layer_zoom: "$zoom",
                                lat: 1, lon: 1, title: 1, name: 1,
                                draggable: {$literal: false},
                                color: {$literal: "cadetblue"},
                                icon: {$literal: "camera"}}},
                    {}
                );
                cursor.each(function (err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc);
                    } else {
                        socket.emit('add-object', doc);
                    }
                });

                // same for the infopanels
                cursor = db.collection('infopanels').aggregate(
                    {$match:{garden:args['garden']}},
                    {$project: {layer_name: {$literal: "infopanels"},
                                layer_zoom: "$zoom",
                                lat: 1, lon: 1, title: 1, text: 1,
                                draggable: {$literal: false},
                                color: {$literal: "purple"},
                                icon: {$literal: "info-sign"}}},
                    {}
                );
                cursor.each(function (err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc);
                    } else {
                        socket.emit('add-object', doc);
                    }
                });

            }});
    });

});
<commit_msg>just some sort of PEP8<commit_after>// This file is part of ghini.web
// http://github.com/Ghini/ghini.web
//
// ghini.web is free software: you can redistribute ghini.web and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or (at
// your option) any later version.
//
// ghini.web is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public
// License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with ghini.web.  If not, see <http://www.gnu.org/licenses/>.
//
// this is the remote server

var http = require('http');
http.globalAgent.maxSockets = 10;

var config = require('./config');
var express = require("express");
var app = express();
var port = Number(process.env.PORT || config.port);  // servicing on port
var dburl = process.env.DATABASE_URL || config.database_url;
console.log(dburl);

var mongodb = require('mongodb');
var dbclient = mongodb.MongoClient;
const mongoose = require('mongoose');

String.prototype.formatU = function() {
    var str = this.toString();
    if (!arguments.length)
        return str;
    var args = typeof arguments[0];
    args = (("string" == args || "number" == args) ? arguments : arguments[0]);
    for (var arg in args)
        str = str.replace(RegExp("\\{" + arg + "\\}", "gi"), args[arg]);
    return str;
};

var sizethree = function (i) {
    var result = '000' + (i || 0);
    return (result).substr(result.length-3);
}

app.set('views', __dirname + '/views');
app.set('view engine', "pug");
app.engine('pug', require('pug').__express);
app.use(express.static(__dirname + '/public'));

var favicon = require('serve-favicon');
app.use(favicon(__dirname + '/public/img/favicon.ico'));

app.get("/", function(req, res){
    res.render("map");
    console.log(dburl);
});

app.get("/panels/:garden_id/:language?", (req, res) => {
    var language = req.params.language || "en";
    if (!language.match("^[a-z][a-z]$")) {
        language = "en";
    }
    var connection = mongoose.createConnection(dburl);
    connection.on('connected', () => {
        var schema = new mongoose.Schema({});
        var Panel = connection.model('infopanels', schema);
        var result = [];

        Panel.
            aggregate([{$match: {garden_id: parseInt(req.params.garden_id)}}]).
            cursor().
            exec().
            on('data', (doc) => {
                doc.url = ("http://www.ghini.me/raw/g_{0}_{1}_{2}.mp3").formatU(
                    sizethree(doc.garden_id), language, sizethree(doc.id_within_garden));
                result.push(doc);
            }).
            on('error', (err) => {
                console.log("err:", err);
            }).
            on('end', () => {
                res.render('panel', {doc: result});
            });
    });
});

app.get("/gardens", (req, res) => {
    var connection = mongoose.createConnection(dburl);
    connection.on('connected', () => {
        var schema = new mongoose.Schema({});
        var Garden = connection.model('garden', schema);
        var result = [];

        Garden.
            aggregate([ {$sort: {name: 1}},
                        {$lookup: {from: "infopanels", foreignField: "garden", localField: "name", as: "infopanels"}},
                        {$project: {name: 1, lat: 1, lon: 1, contact: 1, id: 1,
                                    title: "$name",
                                    infopanels: {$size: "$infopanels"}}} ]).
            cursor().
            exec().
            on('data', (doc) => {result.push(doc)}).
            on('error', (err) => {
                console.log("err:", err);
            }).
            on('end', () => {
                res.render('garden', {doc: result});
            });
    });
});

// make the application listen to the port
// we pass the ExpressJS server to Socket.io. In effect, our real time
// communication will still happen on the same port.
var io = require('socket.io').listen(app.listen(port, function() {
            console.log("Listening on " + port);
        }));

io.sockets.on('connection', function (socket) {
    // Upon a successful connection, we send the list of gardens, with the
    // plants count per garden. gardens are objects as any other object, they
    // have associated the colour red and the icon home, and the zoom at which
    // they appear is 2. then we issue the command 'map-set-view' to the world.

    // As instructed by the final user (perusing garden icons or menu item),
    // the clients issue the 'select-garden' command.  Upon receiving this
    // command from a client, the server issues the command 'map-set-view'
    // (garden position and extent), followed by as many 'add-object'
    // commands as needed to send the lists of objects relative to the
    // selected garden.  These include plants, photos, infopanels.
    dbclient.connect(dburl, function (err, db) {
        if (err) {
            console.log('Unable to connect to the mongoDB server. Error:', err);
        } else {
            // Get the collection of all gardens, each with the amount of plants;
            // We store our find criteria in a cursor.
            // Having a cursor does not mean we performed any database access, yet.
            var cursor = db.collection('gardens').aggregate(
                {$sort: {name: 1}},
                {$lookup: {from: "plants", foreignField: "garden", localField: "name", as: "plants"}},
                {$lookup: {from: "photos", foreignField: "garden", localField: "name", as: "photos"}},
                {$lookup: {from: "infopanels", foreignField: "garden", localField: "name", as: "infopanels"}},
                {$project: {layer_name: {$literal: "gardens"},
                            layer_zoom: {$literal: 1},
                            name: 1, lat: 1, lon: 1, contact: 1,
                            title: "$name",
                            plants: {$size: "$plants"},
                            photos: {$size: "$photos"},
                            infopanels: {$size: "$infopanels"},
                            draggable: {$literal: false},
                            color: {$literal: "red"},
                            icon: {$literal: "home"}}},
                {});
            // Lets iterate on the result.
            // this will access the database, so we act in a callback.
            cursor.each(function (err, doc) {
                if (err || !doc) {
                    console.log("err:", err, "; doc:", doc);
                } else {
                    socket.emit('add-object', doc);
                }
            });
            cursor = db.collection('plants').aggregate(
                {$sort: {code: 1}},
                {$group: {_id: {species: "$species", garden: "$garden"},
                          count: {$sum: 1}, plants: {$push: {_id: "$_id", code: "$code"}}}},
                {$group: {_id: {species: "$_id.species"},
                          gardens: {$push: {name: "$_id.garden", plant_count: "$count", plants: "$plants"}}}},
                {$lookup: {from: "taxa", localField: "_id.species", foreignField: "name", as: "taxon"}},
                {$unwind: {path: "$taxon"}},
                {$project: {_id: "$taxon._id",
                            phonetic: "$taxon.phonetic",
                            species_name: "$taxon.name",
                            family_name: "$taxon.family",
                            gardens: 1, taxon: 1,
                            layer_name: {$literal: "__taxa"}}},
                {$sort: {family_name: 1, species_name: 1}},
                {});
            // Lets iterate on the result.
            // this will access the database, so we act in a callback.
            cursor.each(function (err, doc) {
                if (err || !doc) {
                    console.log("err:", err, "; doc:", doc);
                } else {
                    socket.emit('add-object', doc);
                }
            });
        }});

    socket.on('select-garden', function (args) {
        // require that client wipes all data relative to the current garden
        socket.emit('map-remove-objects', 'plants');
        socket.emit('map-remove-objects', 'photos');
        socket.emit('map-remove-objects', 'infopanels');
        var defaults = {
            zoom: 2,
            lat: 32.0,
            lon: 8.0
        };
        if ('map' in args) {
            Object.assign(args, args.map);
        } else {
            Object.assign(args, defaults);
        }
        if (!('garden' in args)) {
            console.log(args);
            socket.emit('map-set-view', args);
            return;
        }
        dbclient.connect(dburl, function (err, db) {
            if (err) {
                console.log('Unable to connect to the mongoDB server. Error:', err);
            } else {
                // first of all, tell the client to set the view on the
                // garden; the garden document contains lat, lon, and zoom.
                var cursor = db.collection('gardens').findOne({name: args['garden']}, function(err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc, "garden not found", args);
                        console.log('map-set-view', args);
                        socket.emit('map-set-view', args);
                    } else {
                        if ('map' in args) {
                            Object.assign(doc, args.map);
                        }
                        console.log('map-set-view', doc);
                        socket.emit('map-set-view', doc);
                    }
                });

                // Get the plants relative to this garden.
                // We store our find criteria in a cursor.
                // Having a cursor does not mean we performed any database access, yet.
                cursor = db.collection('plants').aggregate(
                    {$match: {garden: args['garden']}},
                    {$lookup: {from: "taxa", localField: "species", foreignField: "name", as: "taxon"}},
                    {$unwind: {path: "$taxon"}},
                    {$project: {layer_name: {$literal: "plants"},
                                layer_zoom: "$zoom",
                                lat: 1, lon: 1, species: 1, taxon: 1, code: 1,
                                title: "$code",
                                species_name: "$taxon.name",
                                vernacular: "$taxon.vernacular",
                                phonetic: "$taxon.phonetic",
                                family: "$taxon.family",
                                draggable: {$literal: false},
                                color: {$literal: "green"},
                                icon: {$literal: "tree-evergreen"}}},
                    {}
                );
                // Lets iterate on the result.
                // this will access the database, so we act in a callback.
                cursor.each(function (err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc);
                    } else {
                        switch(doc.family){
                        case 'Arecaceae':
                            doc.icon = 'palm-tree';
                            break;
                        case 'Musaceae':
                            doc.icon = 'banana-tree';
                            break;
                        case 'Araucariaceae':
                        case 'Cupressaceae':
                        case 'Cycadaceae':
                        case 'Ephedraceae':
                        case 'Ginkgoaceae':
                        case 'Gnetaceae':
                        case 'Pinaceae':
                        case 'Podocarpaceae':
                        case 'Sciadopityaceae':
                        case 'Taxaceae':
                        case 'Welwitschiaceae':
                        case 'Zamiaceae ':
                            doc.icon = 'tree-conifer';
                            break;
                        case 'Anemiaceae':
                        case 'Apleniaceae':
                        case 'Aspleniaceae':
                        case 'Athyriaceae':
                        case 'Blechnaceae':
                        case 'Cibotiaceae':
                        case 'Culcitaceae':
                        case 'Cyatheaceae':
                        case 'Cystodiaceae':
                        case 'Cystopteridaceae':
                        case 'Davalliaceae':
                        case 'Dennstaedtiaceae':
                        case 'Dicksoniaceae':
                        case 'Diplaziopsidaceae':
                        case 'Dipteridaceae':
                        case 'Dryopteridacae':
                        case 'Dryopteridaceae':
                        case 'Equisetaceae':
                        case 'Gleicheniaceae':
                        case 'Hymenophyllaceae':
                        case 'Hypodematiaceae':
                        case 'Isoëtaceae':
                        case 'Lindsaeaceae':
                        case 'Lomariopsidaceae':
                        case 'Lonchitidaceae':
                        case 'Loxsomataceae':
                        case 'Lycopodiaceae':
                        case 'Lygodiaceae':
                        case 'Marattiaceae':
                        case 'Marsileaceae':
                        case 'Matoniaceae':
                        case 'Metaxyaceae':
                        case 'Nephrolepidaceae':
                        case 'Oleandraceae':
                        case 'Onocleaceae':
                        case 'Ophioglossaceae':
                        case 'Osmundaceae':
                        case 'Plagiogyriaceae':
                        case 'Polypodiaceae':
                        case 'Psilotaceae':
                        case 'Pteridaceae':
                        case 'Rhachidosoraceae':
                        case 'Saccolomataceae':
                        case 'Salviniaceae':
                        case 'Schizaeaceae':
                        case 'Selaginellaceae':
                        case 'Tectariaceae':
                        case 'Thelypteridaceae':
                        case 'Thyrsopteridaceae':
                        case 'Woodsiaceae':
                            // doc.icon = 'fern'; // there is no such icon yet
                            break;
                        case 'Asteraceae':
                            doc.icon = 'compositae';
                            break;
                        }
                        socket.emit('add-object', doc);
                    }
                });

                // same for the photos
                cursor = db.collection('photos').aggregate(
                    {$match: {garden: args['garden']}},
                    {$project: {layer_name: {$literal: "photos"},
                                layer_zoom: "$zoom",
                                lat: 1, lon: 1, title: 1, name: 1,
                                draggable: {$literal: false},
                                color: {$literal: "cadetblue"},
                                icon: {$literal: "camera"}}},
                    {}
                );
                cursor.each(function (err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc);
                    } else {
                        socket.emit('add-object', doc);
                    }
                });

                // same for the infopanels
                cursor = db.collection('infopanels').aggregate(
                    {$match: {garden: args['garden']}},
                    {$project: {layer_name: {$literal: "infopanels"},
                                layer_zoom: "$zoom",
                                lat: 1, lon: 1, title: 1, text: 1,
                                draggable: {$literal: false},
                                color: {$literal: "purple"},
                                icon: {$literal: "info-sign"}}},
                    {}
                );
                cursor.each(function (err, doc) {
                    if (err || !doc) {
                        console.log("err:", err, "; doc:", doc);
                    } else {
                        socket.emit('add-object', doc);
                    }
                });

            }});
    });

});
<|endoftext|>


<commit_before>(function(window, document) {
    "use strict";

    var Woopra = {};

    /*
     * Helper functions
     */

    Woopra.CONSTANTS = {
        VERSION: 11,
        ENDPOINT: window.location.protocol + '//www.woopra.com/track/',
        EVENT_ENDPOINT: window.location.protocol + '//www.woopra.com/track/ce/',
        PING_ENDPOINT: window.location.protocol + '//www.woopra.com/track/ping/'
    };

    Woopra.extend = function(o1, o2) {
        for (var key in o2) {
            o1[key] = o2[key];
        }
    };

    Woopra.readCookie = function(name) {
        if (name === '') {
            return '';
        }
        var c = "" + document.cookie;

        var i = c.indexOf(name);
        if (i === -1){
            return "";
        }
        var k = c.indexOf(';', i);
        if (k === -1){
            k = c.length;
        }

        return window.unescape(c.substring(i + name.length + 1, k));
    };

    Woopra.setCookie = function(k, v, exp, domain, path) {
        var cookie = [];
        cookie.push(k + '=' + v);
        cookie.push('expires=' + exp);
        cookie.push('path=' + path);
        cookie.push('domain=.' + domain);

        document.cookie = cookie.join('; ');
    };

    Woopra.getCampaignData = function() {
        var vars = Woopra.getUrlParams();
        var campaign = {};
        var campaignKeys = ['source', 'medium', 'content', 'campaign', 'term'];
        for (var i=0;i<campaignKeys.length; i++) {
            var key = campaignKeys[i];
            var value = vars['utm_' + key] || vars['woo_' + key];
            if (typeof value != 'undefined') {
                campaign['campaign_' + ((key=='campaign')?'name':key)] = value;
            }
        }
        return campaign;
    };

    Woopra.getUrlParams = function() {
        var vars = {};
        window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
            vars[key] = decodeURIComponent(value.split("+").join(" "));
        });
        return vars;
    };

    Woopra.buildUrlParams = function(params, prefix) {
        var _prefix = prefix || '',
            key,
            p = [];

        if (typeof params === 'undefined') {
            return params;
        }

        for (key in params) {
            p.push(_prefix + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
        }
        return p.join('&');
    };

    Woopra.randomString = function() {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
            i,
            rnum,
            s = '';

        for (i = 0; i < 12; i++) {
            rnum = Math.floor(Math.random() * chars.length);
            s += chars.substring(rnum, rnum + 1);
        }

        return s;
    };

    Woopra.loadScript = function(url, callback) {
        var ssc,
            script = document.createElement('script');

        script.type = 'text/javascript';
        script.src = url;
        script.async = true;

        if (typeof script.onreadystatechange != 'undefined') {
            script.onreadystatechange = function() {
                if (this.readyState === 'complete'|| this.readyState === 'loaded') {
                    if (callback) {
                        callback();
                    }
                    Woopra.removeScript(script);
                }
            };
        } else {
            script.onload = function(){
                if (callback) {
                    callback();
                }
                Woopra.removeScript(script);
            };
        }

        ssc = document.getElementsByTagName('script')[0];
        ssc.parentNode.insertBefore(script, ssc);
    };

    Woopra.removeScript = function(script) {
        script.parentNode.removeChild(script);
    };

    Woopra.getHost = function() {
        return window.location.host.replace('www.','');
    };

    Woopra.endsWith = function(str, suffix) {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    };

    var attachEvent = function(el, evt, callback) {
        var attachName,
            eventName,
            other;

        if (typeof window.document.attachEvent !== 'undefined') {
            attachName = 'attachEvent';
            eventName = 'on' + evt;
        }
        else {
            attachName = 'addEventListener';
            eventName = evt;
            other = false;
        }

        el[attachName](eventName, callback, other);
    };

    var Tracker = function(instanceName) {
        this.visitorData = {};
        this.sessionData = {};
        this.options = {};
        this.instanceName = instanceName;
        this.idle = 0;
        this.cookie = '';
        this.last_activity = new Date();
        this._loaded = false;
        this.version = Woopra.CONSTANTS.VERSION;

        if (instanceName && instanceName !== '') {
            window[instanceName] = this;
        }
    };

    Tracker.prototype = {
        init: function() {
            this._setOptions();
            this._processQueue('config');
            this._setupCookie();
            this._bindEvents();
            this._loaded = true;
            this._processQueue();
        },

        /**
         * Sets the initial options
         */
        _setOptions: function() {
            var exp = new Date();

            // Set default options
            exp.setDate(exp.getDate()+365);
            this.config({
                domain : Woopra.getHost(),
                cookie_name : 'wooTracker',
                cookie_domain : null,
                cookie_path : '/',
                cookie_expire : exp,
                ping : true,
                ping_interval : 12000,
                idle_timeout : 300000,
                download_pause : 200,
                outgoing_pause : 400,
                download_tracking : true,
                outgoing_tracking : true,
                ignore_query_url: true
            });
        },

        /**
         * Processes the tracker queue in case user tries to push events
         * before tracker is ready.
         */
        _processQueue: function(type) {
            var i,
                action,
                events,
                _wpt = window._w[this.instanceName];

            if (_wpt && _wpt._e) {
                events = _wpt._e;
                for (i = 0; i < events.length; i++) {
                    action = events[i];
                    if (typeof action !== 'undefined' && this[action[0]] &&
                        (typeof type === 'undefined' || type === action[0])) {
                        this[action[0]].apply(this, Array.prototype.slice.call(action, 1));
                    }
                }
            }
        },

        /**
         * Sets up the tracking cookie
         */
        _setupCookie: function() {
            // Setup cookie
            this.cookie = Woopra.readCookie(this.config('cookie_name'));
            if (this.cookie && this.cookie.length > 0) {
            }
            else {
                this.cookie = Woopra.randomString();
            }

            if (this.config('cookie_domain') === null) {
                if (Woopra.endsWith(window.location.host, '.' + this.config('domain'))) {
                    this.config('cookie_domain', this.config('domain'));
                } else {
                    this.config('cookie_domain', Woopra.getHost());
                }
            }
            Woopra.setCookie(
                this.config('cookie_name'),
                this.cookie,
                this.config('cookie_exp'),
                this.config('cookie_domain'),
                this.config('cookie_path')
            );
        },

        /**
         * Binds some events to measure mouse and keyboard events
         */
        _bindEvents: function() {
            var self = this;

            attachEvent(document, 'mousedown', function() {
                self.clicked.apply(self, arguments);
            });
            attachEvent(document, 'mousemove', function() {
                self.moved.apply(self, arguments);
            });
            attachEvent(document, 'keydown', function() {
                self.typed.apply(self, arguments);
            });
        },

        /**
         * Sets/gets values from dataStore depending on arguments passed
         *
         * @param dataStore Object The tracker property to read/write
         * @param key String/Object Returns property object if key and value is undefined,
         *      acts as a getter if only `key` is defined and a string, and
         *      acts as a setter if `key` and `value` are defined OR if `key` is an object.
         */
        _dataSetter: function(dataStore, key, value) {
            var i;

            if (typeof key === 'undefined') {
                return dataStore;
            }

            if (typeof value === 'undefined') {
                if (typeof key === 'string') {
                    return dataStore[key];
                }
                if (typeof key === 'object') {
                    for (i in key) {
                        dataStore[i] = key[i];
                        //this._dataSetter(dataStore, i, key[i]);
                    }
                }
            }
            else {
                dataStore[key] = value;
            }

            return this;
        },

        /**
         * Builds the correct tracking Url and performs an HTTP request
         */
        _push: function(endpoint, options) {
            var _options = options || {},
                _endpoint = Woopra.CONSTANTS.ENDPOINT + endpoint + '/',
                random = 'ra=' + Woopra.randomString(),
                queryString,
                scriptUrl,
                data = [];

            data.push(random);
            data.push(Woopra.buildUrlParams(this.getOptionParams()));

            if (_options.visitorData) {
                data.push(Woopra.buildUrlParams(_options.visitorData, 'cv_'));
            }

            if (_options.eventData) {
                data.push(Woopra.buildUrlParams(_options.eventData, 'ce_'));
            }

            queryString = '?' + data.join('&');

            scriptUrl = _endpoint + queryString;
            Woopra.loadScript(scriptUrl, _options.callback);
        },

        /**
         * Sets configuration options
         */
        config: function(key, value) {
            return this._dataSetter(this.options, key, value);
        },

        /**
         * Use to attach custom visit data that doesn't stick to visitor
         * ** Not in use yet
         */
        visit: function(key, value) {
            return this._dataSetter(this.sessionData, key, value);
        },

        /**
         * Attach custom visitor data
         */
        identify: function(key, value) {
            var blah = this._dataSetter(this.visitorData, key, value);

            return blah;
        },

        call: function() {
        },

        track: function(name, options) {
            var event = {},
                callback;

            // Load campaign params (load first to allow overrides)
            Woopra.extend(event, Woopra.getCampaignData());

            // Track default: pageview
            if (typeof name === 'undefined') {
                event.name = 'pv',
                event.url = this.getPageUrl();
                event.title = this.getPageTitle();
            }
            // Track custom events
            else if (options === 'undefined') {
                if (typeof name === 'string') {
                    event.name = name;
                }
                if (typeof name === 'object') {
                    this._dataSetter(event, name);
                }
            }
            // Track custom events in format of name,object
            else {
                event.name = name;
                this._dataSetter(event, options);
            }

            // Extract callback
            if (typeof event.hitCallback === 'function') {
                callback = event.hitCallback;
                delete event.hitCallback;
            }

            this._push('ce', {
                visitorData: this.visitorData,
                eventData: event,
                callback: callback
            });

            this.startPing();
        },

        startPing: function() {
            var self = this;
            if (typeof this.pingInterval == 'undefined') {
                window.clearInterval(this.pingInterval);
                delete this.pingInterval;
            }
            this.pingInterval = window.setInterval(function() {
                self.ping();
            }, this.config('ping_interval'));
        },

        stopPing: function() {
            if (typeof this.pingInterval !== 'undefined') {
                window.clearInterval(this.pingInterval);
                delete this.pingInterval;
            }
        },

        /**
         * Pings tracker with visitor info
         */
        ping: function() {
            var now;

            if (this.config('ping') && this.idle < this.config('idle_timeout')) {
                this._push('ping', {
                    visitorData: this.visitorData
                });
            }

            now = new Date();
            if (now - this.last_activity > 10000) {
                this.idle = now - this.last_activity;
            }

            return this;
        },

        /**
         * Pushes visitor data to server without sending an event
         */
        push: function() {
            this._push('identify', {
                visitorData: this.visitorData
            });
            return this;
        },

        /**
         * synchronous sleep
         */
        sleep: function(millis) {
            var date = new Date(),
                curDate = new Date();

            while (curDate-date < millis) {
                curDate = new Date();
            }
        },

        // User Action tracking

        /**
         * Measure when the user last moved their mouse to update idle state
         */
        moved: function() {
            this.last_activity = new Date();
            this.idle = 0;
        },

        /**
         * Measure when user last typed
         */
        typed: function() {
            this.vs = 2;
        },

        /**
         * Record clicks to check for downloads and outgoing links
         */
        clicked: function(e) {
            var cElem,
                link,
                _download,
                ev;

            this.moved();

            cElem = e.srcElement || e.target;
            while (typeof cElem !== 'undefined' && cElem !== null) {
                if (cElem.tagName === 'a') {
                    break;
                }
                cElem = cElem.parentNode;
            }

            if (typeof cElem !== 'undefined' && cElem !== null) {
                link = cElem;
                _download = link.pathname.match(/(?:doc|dmg|eps|jpg|jpeg|png|svg|xls|ppt|pdf|xls|zip|txt|vsd|vxd|js|css|rar|exe|wma|mov|avi|wmv|mp3|mp4|m4v)($|\&)/);
                ev = false;
                if (this.config('download_tracking')) {
                    if (_download &&
                        link.href.toString().indexOf('woopra-ns.com') < 0) {
                        this.track('download', {
                            url: link.href
                        });
                        this.sleep(this.config('download_pause'));
                    }
                }
                if (this.config('outgoing_tracking')) {
                    if (!_download &&
                        link.hostname !== window.location.host &&
                        link.hostname.indexOf('javascript') === -1 &&
                        link.hostname !== '') {
                        this.track('outgoing', {
                            url: link.href
                        });
                        this.sleep(this.config('outgoing_pause'));
                    }
                }
            }
        },

        getPageUrl: function() {
            if (this.options.ignore_query_url) {
                return window.location.pathname;
            } else {
                return window.location.pathname + window.location.search;
            }
        },

        getPageTitle: function() {
            return (document.getElementsByTagName('title').length === 0) ? '' : document.getElementsByTagName('title')[0].innerHTML;
        },

        getOptionParams: function() {
            var o = {
                alias: this.config('domain'),
                instance: this.instanceName,
                cookie: Woopra.readCookie(this.config('cookie_name')),
                meta: Woopra.readCookie('wooMeta') || '',
                screen: window.screen.width + 'x' + window.screen.height,
                language: window.navigator.browserLanguage || window.navigator.language || "",
                referer: document.referrer,
                idle: '' + parseInt(this.idle/1000, 10),
                vs: 'i'
            };

            // this.vs is 2 after typing so 'writing'
            if (this.vs === 2) {
                o.vs = 'w';
                this.vs = 0;
            } else {
                if (this.idle === 0) {
                    o.vs = 'r';
                }
            }

            return o;
        }
    };

    //Woopra.Tracker = Tracker;
    var _public = {
        Tracker: Tracker
    };

    // Initialize instances & preloaded settings/events
    if (typeof window._w !== 'undefined') {
        for (var name in window._w) {
            var instance = new Tracker(name);
            instance.init();
        }
    }

    window.WoopraTracker = Tracker;

    if (typeof window.exports !== 'undefined') {
        Woopra.Tracker = Tracker;
        window.exports.Woopra = Woopra;
    }

})(window, document);
<commit_msg>Fix ping, we should only ever have 1 ping timer<commit_after>(function(window, document) {
    "use strict";

    var Woopra = {};

    /*
     * Helper functions
     */

    Woopra.CONSTANTS = {
        VERSION: 11,
        ENDPOINT: window.location.protocol + '//www.woopra.com/track/',
        EVENT_ENDPOINT: window.location.protocol + '//www.woopra.com/track/ce/',
        PING_ENDPOINT: window.location.protocol + '//www.woopra.com/track/ping/'
    };

    Woopra.extend = function(o1, o2) {
        for (var key in o2) {
            o1[key] = o2[key];
        }
    };

    Woopra.readCookie = function(name) {
        if (name === '') {
            return '';
        }
        var c = "" + document.cookie;

        var i = c.indexOf(name);
        if (i === -1){
            return "";
        }
        var k = c.indexOf(';', i);
        if (k === -1){
            k = c.length;
        }

        return window.unescape(c.substring(i + name.length + 1, k));
    };

    Woopra.setCookie = function(k, v, exp, domain, path) {
        var cookie = [];
        cookie.push(k + '=' + v);
        cookie.push('expires=' + exp);
        cookie.push('path=' + path);
        cookie.push('domain=.' + domain);

        document.cookie = cookie.join('; ');
    };

    Woopra.getCampaignData = function() {
        var vars = Woopra.getUrlParams();
        var campaign = {};
        var campaignKeys = ['source', 'medium', 'content', 'campaign', 'term'];
        for (var i=0;i<campaignKeys.length; i++) {
            var key = campaignKeys[i];
            var value = vars['utm_' + key] || vars['woo_' + key];
            if (typeof value != 'undefined') {
                campaign['campaign_' + ((key=='campaign')?'name':key)] = value;
            }
        }
        return campaign;
    };

    Woopra.getUrlParams = function() {
        var vars = {};
        window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
            vars[key] = decodeURIComponent(value.split("+").join(" "));
        });
        return vars;
    };

    Woopra.buildUrlParams = function(params, prefix) {
        var _prefix = prefix || '',
            key,
            p = [];

        if (typeof params === 'undefined') {
            return params;
        }

        for (key in params) {
            p.push(_prefix + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]));
        }
        return p.join('&');
    };

    Woopra.randomString = function() {
        var chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
            i,
            rnum,
            s = '';

        for (i = 0; i < 12; i++) {
            rnum = Math.floor(Math.random() * chars.length);
            s += chars.substring(rnum, rnum + 1);
        }

        return s;
    };

    Woopra.loadScript = function(url, callback) {
        var ssc,
            script = document.createElement('script');

        script.type = 'text/javascript';
        script.src = url;
        script.async = true;

        if (typeof script.onreadystatechange != 'undefined') {
            script.onreadystatechange = function() {
                if (this.readyState === 'complete'|| this.readyState === 'loaded') {
                    if (callback) {
                        callback();
                    }
                    Woopra.removeScript(script);
                }
            };
        } else {
            script.onload = function(){
                if (callback) {
                    callback();
                }
                Woopra.removeScript(script);
            };
        }

        ssc = document.getElementsByTagName('script')[0];
        ssc.parentNode.insertBefore(script, ssc);
    };

    Woopra.removeScript = function(script) {
        script.parentNode.removeChild(script);
    };

    Woopra.getHost = function() {
        return window.location.host.replace('www.','');
    };

    Woopra.endsWith = function(str, suffix) {
        return str.indexOf(suffix, str.length - suffix.length) !== -1;
    };

    var attachEvent = function(el, evt, callback) {
        var attachName,
            eventName,
            other;

        if (typeof window.document.attachEvent !== 'undefined') {
            attachName = 'attachEvent';
            eventName = 'on' + evt;
        }
        else {
            attachName = 'addEventListener';
            eventName = evt;
            other = false;
        }

        el[attachName](eventName, callback, other);
    };

    var Tracker = function(instanceName) {
        this.visitorData = {};
        this.sessionData = {};
        this.options = {};
        this.instanceName = instanceName;
        this.idle = 0;
        this.cookie = '';
        this.last_activity = new Date();
        this._loaded = false;
        this.version = Woopra.CONSTANTS.VERSION;

        if (instanceName && instanceName !== '') {
            window[instanceName] = this;
        }
    };

    Tracker.prototype = {
        init: function() {
            this._setOptions();
            this._processQueue('config');
            this._setupCookie();
            this._bindEvents();
            this._loaded = true;
            this._processQueue();
        },

        /**
         * Sets the initial options
         */
        _setOptions: function() {
            var exp = new Date();

            // Set default options
            exp.setDate(exp.getDate()+365);
            this.config({
                domain : Woopra.getHost(),
                cookie_name : 'wooTracker',
                cookie_domain : null,
                cookie_path : '/',
                cookie_expire : exp,
                ping : true,
                ping_interval : 12000,
                idle_timeout : 300000,
                download_pause : 200,
                outgoing_pause : 400,
                download_tracking : true,
                outgoing_tracking : true,
                ignore_query_url: true
            });
        },

        /**
         * Processes the tracker queue in case user tries to push events
         * before tracker is ready.
         */
        _processQueue: function(type) {
            var i,
                action,
                events,
                _wpt = window._w[this.instanceName];

            if (_wpt && _wpt._e) {
                events = _wpt._e;
                for (i = 0; i < events.length; i++) {
                    action = events[i];
                    if (typeof action !== 'undefined' && this[action[0]] &&
                        (typeof type === 'undefined' || type === action[0])) {
                        this[action[0]].apply(this, Array.prototype.slice.call(action, 1));
                    }
                }
            }
        },

        /**
         * Sets up the tracking cookie
         */
        _setupCookie: function() {
            // Setup cookie
            this.cookie = Woopra.readCookie(this.config('cookie_name'));
            if (this.cookie && this.cookie.length > 0) {
            }
            else {
                this.cookie = Woopra.randomString();
            }

            if (this.config('cookie_domain') === null) {
                if (Woopra.endsWith(window.location.host, '.' + this.config('domain'))) {
                    this.config('cookie_domain', this.config('domain'));
                } else {
                    this.config('cookie_domain', Woopra.getHost());
                }
            }
            Woopra.setCookie(
                this.config('cookie_name'),
                this.cookie,
                this.config('cookie_exp'),
                this.config('cookie_domain'),
                this.config('cookie_path')
            );
        },

        /**
         * Binds some events to measure mouse and keyboard events
         */
        _bindEvents: function() {
            var self = this;

            attachEvent(document, 'mousedown', function() {
                self.clicked.apply(self, arguments);
            });
            attachEvent(document, 'mousemove', function() {
                self.moved.apply(self, arguments);
            });
            attachEvent(document, 'keydown', function() {
                self.typed.apply(self, arguments);
            });
        },

        /**
         * Sets/gets values from dataStore depending on arguments passed
         *
         * @param dataStore Object The tracker property to read/write
         * @param key String/Object Returns property object if key and value is undefined,
         *      acts as a getter if only `key` is defined and a string, and
         *      acts as a setter if `key` and `value` are defined OR if `key` is an object.
         */
        _dataSetter: function(dataStore, key, value) {
            var i;

            if (typeof key === 'undefined') {
                return dataStore;
            }

            if (typeof value === 'undefined') {
                if (typeof key === 'string') {
                    return dataStore[key];
                }
                if (typeof key === 'object') {
                    for (i in key) {
                        dataStore[i] = key[i];
                        //this._dataSetter(dataStore, i, key[i]);
                    }
                }
            }
            else {
                dataStore[key] = value;
            }

            return this;
        },

        /**
         * Builds the correct tracking Url and performs an HTTP request
         */
        _push: function(endpoint, options) {
            var _options = options || {},
                _endpoint = Woopra.CONSTANTS.ENDPOINT + endpoint + '/',
                random = 'ra=' + Woopra.randomString(),
                queryString,
                scriptUrl,
                data = [];

            data.push(random);
            data.push(Woopra.buildUrlParams(this.getOptionParams()));

            if (_options.visitorData) {
                data.push(Woopra.buildUrlParams(_options.visitorData, 'cv_'));
            }

            if (_options.eventData) {
                data.push(Woopra.buildUrlParams(_options.eventData, 'ce_'));
            }

            queryString = '?' + data.join('&');

            scriptUrl = _endpoint + queryString;
            Woopra.loadScript(scriptUrl, _options.callback);
        },

        /**
         * Sets configuration options
         */
        config: function(key, value) {
            return this._dataSetter(this.options, key, value);
        },

        /**
         * Use to attach custom visit data that doesn't stick to visitor
         * ** Not in use yet
         */
        visit: function(key, value) {
            return this._dataSetter(this.sessionData, key, value);
        },

        /**
         * Attach custom visitor data
         */
        identify: function(key, value) {
            var blah = this._dataSetter(this.visitorData, key, value);

            return blah;
        },

        call: function() {
        },

        track: function(name, options) {
            var event = {},
                callback;

            // Load campaign params (load first to allow overrides)
            Woopra.extend(event, Woopra.getCampaignData());

            // Track default: pageview
            if (typeof name === 'undefined') {
                event.name = 'pv',
                event.url = this.getPageUrl();
                event.title = this.getPageTitle();
            }
            // Track custom events
            else if (options === 'undefined') {
                if (typeof name === 'string') {
                    event.name = name;
                }
                if (typeof name === 'object') {
                    this._dataSetter(event, name);
                }
            }
            // Track custom events in format of name,object
            else {
                event.name = name;
                this._dataSetter(event, options);
            }

            // Extract callback
            if (typeof event.hitCallback === 'function') {
                callback = event.hitCallback;
                delete event.hitCallback;
            }

            this._push('ce', {
                visitorData: this.visitorData,
                eventData: event,
                callback: callback
            });

            this.startPing();
        },

        startPing: function() {
            var self = this;

            if (typeof this.pingInterval === 'undefined') {
                this.pingInterval = window.setInterval(function() {
                    self.ping();
                }, this.config('ping_interval'));
            }
        },

        stopPing: function() {
            if (typeof this.pingInterval !== 'undefined') {
                window.clearInterval(this.pingInterval);
                delete this.pingInterval;
            }
        },

        /**
         * Pings tracker with visitor info
         */
        ping: function() {
            var now;

            if (this.config('ping') && this.idle < this.config('idle_timeout')) {
                this._push('ping', {
                    visitorData: this.visitorData
                });
            }

            now = new Date();
            if (now - this.last_activity > 10000) {
                this.idle = now - this.last_activity;
            }

            return this;
        },

        /**
         * Pushes visitor data to server without sending an event
         */
        push: function() {
            this._push('identify', {
                visitorData: this.visitorData
            });
            return this;
        },

        /**
         * synchronous sleep
         */
        sleep: function(millis) {
            var date = new Date(),
                curDate = new Date();

            while (curDate-date < millis) {
                curDate = new Date();
            }
        },

        // User Action tracking

        /**
         * Measure when the user last moved their mouse to update idle state
         */
        moved: function() {
            this.last_activity = new Date();
            this.idle = 0;
        },

        /**
         * Measure when user last typed
         */
        typed: function() {
            this.vs = 2;
        },

        /**
         * Record clicks to check for downloads and outgoing links
         */
        clicked: function(e) {
            var cElem,
                link,
                _download,
                ev;

            this.moved();

            cElem = e.srcElement || e.target;
            while (typeof cElem !== 'undefined' && cElem !== null) {
                if (cElem.tagName === 'a') {
                    break;
                }
                cElem = cElem.parentNode;
            }

            if (typeof cElem !== 'undefined' && cElem !== null) {
                link = cElem;
                _download = link.pathname.match(/(?:doc|dmg|eps|jpg|jpeg|png|svg|xls|ppt|pdf|xls|zip|txt|vsd|vxd|js|css|rar|exe|wma|mov|avi|wmv|mp3|mp4|m4v)($|\&)/);
                ev = false;
                if (this.config('download_tracking')) {
                    if (_download &&
                        link.href.toString().indexOf('woopra-ns.com') < 0) {
                        this.track('download', {
                            url: link.href
                        });
                        this.sleep(this.config('download_pause'));
                    }
                }
                if (this.config('outgoing_tracking')) {
                    if (!_download &&
                        link.hostname !== window.location.host &&
                        link.hostname.indexOf('javascript') === -1 &&
                        link.hostname !== '') {
                        this.track('outgoing', {
                            url: link.href
                        });
                        this.sleep(this.config('outgoing_pause'));
                    }
                }
            }
        },

        getPageUrl: function() {
            if (this.options.ignore_query_url) {
                return window.location.pathname;
            } else {
                return window.location.pathname + window.location.search;
            }
        },

        getPageTitle: function() {
            return (document.getElementsByTagName('title').length === 0) ? '' : document.getElementsByTagName('title')[0].innerHTML;
        },

        getOptionParams: function() {
            var o = {
                alias: this.config('domain'),
                instance: this.instanceName,
                cookie: Woopra.readCookie(this.config('cookie_name')),
                meta: Woopra.readCookie('wooMeta') || '',
                screen: window.screen.width + 'x' + window.screen.height,
                language: window.navigator.browserLanguage || window.navigator.language || "",
                referer: document.referrer,
                idle: '' + parseInt(this.idle/1000, 10),
                vs: 'i'
            };

            // this.vs is 2 after typing so 'writing'
            if (this.vs === 2) {
                o.vs = 'w';
                this.vs = 0;
            } else {
                if (this.idle === 0) {
                    o.vs = 'r';
                }
            }

            return o;
        }
    };

    //Woopra.Tracker = Tracker;
    var _public = {
        Tracker: Tracker
    };

    // Initialize instances & preloaded settings/events
    if (typeof window._w !== 'undefined') {
        for (var name in window._w) {
            var instance = new Tracker(name);
            instance.init();
        }
    }

    window.WoopraTracker = Tracker;

    if (typeof window.exports !== 'undefined') {
        Woopra.Tracker = Tracker;
        window.exports.Woopra = Woopra;
    }

})(window, document);
<|endoftext|>


<commit_before>/// Require necessary dependencies
var fs = require('fs');
var koa = require('koa');
var server = koa();
var serve = require('koa-static');
var staticCache = require('koa-static-cache');
var enforceHTTPS = require('koa-sslify');

/// Force HTTPS on all requests
server.use(enforceHTTPS({
	trustProtoHeader: true
}));

/// Example API request handling
/*server.use(function *(next) {
	if(this.request.path !== '/api/roles') return yield next;

	var data = yield function (next) {
		fs.readFile(__dirname + '/roles.json', function(err, data) {
			if(err) next(err, null);
			else next(null, JSON.parse(data));
		});
	};

	this.body = data;
});*/

server.use(function* (next) {
//
//

//

// 	'/': App.Intro,
// 	'/interface/:name': App.Interface,

	yield next;
});

/// Redirect non-file-ish queries to index, otherwise load .html and .json content for articles
server.use(function* (next) {
	if(this.request.path.length && /^(\/[\-_a-z0-9]+)+\/?$/i.test(this.request.path))
	{
		var index = yield readFile(__dirname + '/frontend/index.html');

		var route = this.request.path.match(/^\/interface\/([\-_a-z0-9]+)$/i);
		var exists = false;
		if(route) exists = yield fileExists(__dirname + '/frontend/docs/' + route[1] + '.html');

		if(route)
		{
			if(exists)
			{
				var article = yield readFile(__dirname + '/frontend/docs/' + route[1] + '.html');
				var jsonLD = yield readFile(__dirname + '/frontend/docs/' + route[1] + '.json');

				if(jsonLD) index = index.replace('', '');
				if(article)
				{
					article = article
						.replace(/\s+/gm, '')
						.replace(/\s+<\/code>/gm, '');
					index = index.replace('', article);
				}

				this.body = index;
			}

			else
			{
				index = index.replace('', '
Not Found

Back to homepage?
');

				this.status = 404;
				this.body = index;
			}
		}

		else
		{
			this.request.path = '/';
			yield next;
		}
	}

	else yield next;
});

server.use(function* (next) {
	if(this.request.path === '/')
	{
		var index = yield readFile(__dirname + '/frontend/index.html');
		var article = yield readFile(__dirname + '/frontend/docs/Default.html');
		var jsonLD = yield readFile(__dirname + '/frontend/docs/Default.json');

		if(jsonLD) index = index.replace('', '');
		if(article) index = index.replace('', article);

		this.body = index;
	}

	else yield next;
});

function fileExists(path) {
	return new Promise(function(resolve, reject) {
		fs.stat(path, function(err, stats) {
			resolve(stats? stats.isFile(): false);
		});
	});
}

function readFile(path) {
	return new Promise(function(resolve, reject) {
		fs.readFile(path, { encoding: 'utf8' }, function(err, data) {
			/*if(err) reject(err); else*/ resolve(data);
		})
	})
}

/*function readJSON(path) {
	return new Promise(function(resolve, reject) {
		fs.readFile(path, { encoding: 'utf8' }, function(err, data) {
			if(err) reject(err);
			var json;
			try { json = JSON.parse(data) }
			catch(e) { return reject(e) }
			resolve(json);
		})
	})
}

server.use(function* (next) {
	if(this.request.path && this.request.path === '/documentation.json')
	{
		var docs = yield readJSON(__dirname + '/frontend/documentation.json');
		this.body = docs;
	}

	else yield next;
})*/

if(process.env.NODE_ENV === 'production')
{
	server.use(staticCache('./frontend', {
		buffer: true,
		gzip: true
// 		maxAge: 4 * 24 * 60 * 60
	}));
}

/// Define a static directory to serve
server.use(serve('./frontend'));

if(!module.parent)
{
	/// Launch the server locally
	var port = process.env.PORT || 8045;
	server.listen(port);
	console.log('Launching webserver at localhost:' + port);
}
else
{
	/// Export the koa server
	module.exports = server;
}
<commit_msg>Update port for server<commit_after>/// Require necessary dependencies
var fs = require('fs');
var koa = require('koa');
var server = koa();
var serve = require('koa-static');
var staticCache = require('koa-static-cache');
var enforceHTTPS = require('koa-sslify');

/// Force HTTPS on all requests
server.use(enforceHTTPS({
	trustProtoHeader: true
}));

/// Example API request handling
/*server.use(function *(next) {
	if(this.request.path !== '/api/roles') return yield next;

	var data = yield function (next) {
		fs.readFile(__dirname + '/roles.json', function(err, data) {
			if(err) next(err, null);
			else next(null, JSON.parse(data));
		});
	};

	this.body = data;
});*/

server.use(function* (next) {
//
//

//

// 	'/': App.Intro,
// 	'/interface/:name': App.Interface,

	yield next;
});

/// Redirect non-file-ish queries to index, otherwise load .html and .json content for articles
server.use(function* (next) {
	if(this.request.path.length && /^(\/[\-_a-z0-9]+)+\/?$/i.test(this.request.path))
	{
		var index = yield readFile(__dirname + '/frontend/index.html');

		var route = this.request.path.match(/^\/interface\/([\-_a-z0-9]+)$/i);
		var exists = false;
		if(route) exists = yield fileExists(__dirname + '/frontend/docs/' + route[1] + '.html');

		if(route)
		{
			if(exists)
			{
				var article = yield readFile(__dirname + '/frontend/docs/' + route[1] + '.html');
				var jsonLD = yield readFile(__dirname + '/frontend/docs/' + route[1] + '.json');

				if(jsonLD) index = index.replace('', '');
				if(article)
				{
					article = article
						.replace(/\s+/gm, '')
						.replace(/\s+<\/code>/gm, '');
					index = index.replace('', article);
				}

				this.body = index;
			}

			else
			{
				index = index.replace('', '
Not Found

Back to homepage?
');

				this.status = 404;
				this.body = index;
			}
		}

		else
		{
			this.request.path = '/';
			yield next;
		}
	}

	else yield next;
});

server.use(function* (next) {
	if(this.request.path === '/')
	{
		var index = yield readFile(__dirname + '/frontend/index.html');
		var article = yield readFile(__dirname + '/frontend/docs/Default.html');
		var jsonLD = yield readFile(__dirname + '/frontend/docs/Default.json');

		if(jsonLD) index = index.replace('', '');
		if(article) index = index.replace('', article);

		this.body = index;
	}

	else yield next;
});

function fileExists(path) {
	return new Promise(function(resolve, reject) {
		fs.stat(path, function(err, stats) {
			resolve(stats? stats.isFile(): false);
		});
	});
}

function readFile(path) {
	return new Promise(function(resolve, reject) {
		fs.readFile(path, { encoding: 'utf8' }, function(err, data) {
			/*if(err) reject(err); else*/ resolve(data);
		})
	})
}

/*function readJSON(path) {
	return new Promise(function(resolve, reject) {
		fs.readFile(path, { encoding: 'utf8' }, function(err, data) {
			if(err) reject(err);
			var json;
			try { json = JSON.parse(data) }
			catch(e) { return reject(e) }
			resolve(json);
		})
	})
}

server.use(function* (next) {
	if(this.request.path && this.request.path === '/documentation.json')
	{
		var docs = yield readJSON(__dirname + '/frontend/documentation.json');
		this.body = docs;
	}

	else yield next;
})*/

if(process.env.NODE_ENV === 'production')
{
	server.use(staticCache('./frontend', {
		buffer: true,
		gzip: true
// 		maxAge: 4 * 24 * 60 * 60
	}));
}

/// Define a static directory to serve
server.use(serve('./frontend'));

if(!module.parent)
{
	/// Launch the server locally
	var port = process.env.PORT || 8039;
	server.listen(port);
	console.log('Launching webserver at localhost:' + port);
}
else
{
	/// Export the koa server
	module.exports = server;
}
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http:// mozilla.org/MPL/2.0/. */

define(["jquery", "util", "session", "elementFinder"],
function ($, util, session, elementFinder) {

  // constant var to indicate whether two players are too far apart in sync
  var TOO_FAR_APART = 3000;
  // embedded youtube iframes
  var youTubeIframes = [];
  // youtube API load delay
  var API_LOADING_DELAY = 2000;

  session.on("reinitialize", function () {
    if (TogetherJS.config.get("youtube")) {
      prepareYouTube();
    }
  });

  session.on("close", function () {
    $(youTubeIframes).each(function (i, iframe) {
      // detach players from iframes
      $(iframe).removeData("togetherjs-player");
      $(iframe).removeData("dontPublish");
      $(iframe).removeData("currentVideoId");
      // disable iframeAPI
      $(iframe).removeAttr("enablejsapi");
      // remove unique youtube iframe indicators
      var id = $(iframe).attr("id");
      if (id.indexOf("youtube-player") === 0) {
        // An id we added
        $(iframe).removeAttr("id");
      }
      youTubeIframes = [];
    });
  });

  TogetherJS.config.track("youtube", function (track, previous) {
    if (track && ! previous) {
      prepareYouTube();
      // You can enable youtube dynamically, but can't turn it off:
      TogetherJS.config.close("youtube");
    }
  });

  function prepareYouTube() {
    // setup iframes first
    setupYouTubeIframes();

    // this function should be global so it can be called when API is loaded
    window.onYouTubeIframeAPIReady = function() {
      // YouTube API is ready
      $(youTubeIframes).each(function (i, iframe) {
        var player = new YT.Player(iframe.id, { // get the reference to the already existing iframe
          events: {
            'onReady': insertPlayer,
            'onStateChange': publishPlayerStateChange
          }
        });
      });
    };

    if (window.YT === undefined) {
      // load necessary API
      // it calls onYouTubeIframeAPIReady automatically when the API finishes loading
      var tag = document.createElement('script');
      tag.src = "https://www.youtube.com/iframe_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    } else {
      // manually invoke APIReady function when the API was already loaded by user
      onYouTubeIframeAPIReady();
    }

    // give each youtube iframe a unique id and set its enablejsapi param to true
    function setupYouTubeIframes() {
      var iframes = $('iframe');
      iframes.each(function (i, iframe) {
        // look for YouTube Iframes
        // if the iframe's unique id is already set, skip it
        if ($(iframe).attr("src").indexOf("youtube") != -1 && !$(iframe).attr("id")) {
          $(iframe).attr("id", "youtube-player"+i);
          $(iframe).attr("ensablejsapi", 1);
          youTubeIframes[i] = iframe;
        }
      });
    } // iframes are ready

    function insertPlayer(event) {
      // only when it is READY, attach a player to its iframe
      var currentPlayer = event.target;
      var currentIframe = currentPlayer.a;
      // check if a player is already attached in case of being reinitialized
      if (!$(currentIframe).data("togetherjs-player")) {
        $(currentIframe).data("togetherjs-player", currentPlayer);
        // initialize its dontPublish flag as well
        $(currentIframe).data("dontPublish", false);
        // store its current video's id
        var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
        $(currentIframe).data("currentVideoId", currentVideoId);
      }
    }
  } // end of prepareYouTube

  function publishPlayerStateChange(event) {
    var currentPlayer = event.target;
    var currentIframe = currentPlayer.a;
    var currentTime = currentPlayer.getCurrentTime();
    var iframeLocation = elementFinder.elementLocation(currentIframe);

    if ($(currentPlayer).data("seek")) {
      $(currentPlayer).removeData("seek");
      return;
    }

    // do not publish if playerState was changed by other users
    if ($(currentIframe).data("dontPublish")) {
      // make it false again so it can start publishing events of its own state changes
      $(currentIframe).data("dontPublish", false);
      return;
    }

    // notify other people that I changed the player state
    if (event.data == YT.PlayerState.PLAYING) {

      var currentVideoId = isDifferentVideoLoaded(currentIframe);
      if (currentVideoId) {
        // notify that I just loaded another video
        publishDifferentVideoLoaded(iframeLocation, currentVideoId);
        // update current video id
        $(currentIframe).data("currentVideoId", currentVideoId);
      } else {
        session.send({
          type: "playerStateChange",
          element: iframeLocation,
          playerState: 1,
          playerTime: currentTime
        });
      }
    } else if (event.data == YT.PlayerState.PAUSED) {
      session.send({
        type: "playerStateChange",
        element: iframeLocation,
        playerState: 2,
        playerTime: currentTime
      });
    } else {
      // do nothing when the state is buffering, cued, or ended
      return;
    }
  }

  function publishDifferentVideoLoaded(iframeLocation, videoId) {
    session.send({
      type: "differentVideoLoaded",
      videoId: videoId,
      element: iframeLocation
    });
  }

  session.hub.on('playerStateChange', function (msg) {
    var iframe = elementFinder.findElement(msg.element);
    var player = $(iframe).data("togetherjs-player");
    var currentTime = player.getCurrentTime();
    var currentState = player.getPlayerState();

    if (currentState != msg.playerState) {
      $(iframe).data("dontPublish", true);
    }

    if (msg.playerState == 1) {
      player.playVideo();
      // seekTo() updates the video's time and plays it if it was already playing
      // and pauses it if it was already paused
      if (areTooFarApart(currentTime, msg.playerTime)) {
        player.seekTo(msg.playerTime, true);
      }
    } else if (msg.playerState == 2) {
      // When YouTube videos are advanced while playing,
      // Chrome: pause -> pause -> play (onStateChange is called even when it is from pause to pause)
      // FireFox: buffering -> play -> buffering -> play
      // We must prevent advanced videos from going out of sync
      player.pauseVideo();
      if (areTooFarApart(currentTime, msg.playerTime)) {
        // "seek" flag will help supress publishing unwanted state changes
        $(player).data("seek", true);
        player.seekTo(msg.playerTime, true);
      }
    }
  });

  // if a late user joins a channel, synchronize his videos
  session.hub.on('hello', function () {
    // wait a couple seconds to make sure the late user has finished loading API
    setTimeout(synchronizeVideosOfLateGuest, API_LOADING_DELAY);
  });

  session.hub.on('synchronizeVideosOfLateGuest', function (msg) {
    var iframe = elementFinder.findElement(msg.element);
    var player = $(iframe).data("togetherjs-player");
    // check if another video had been loaded to an existing iframe before I joined
    var currentVideoId = $(iframe).data("currentVideoId");
    if (msg.videoId != currentVideoId) {
      $(iframe).data("currentVideoId", msg.videoId);
      player.loadVideoById(msg.videoId, msg.playerTime, 'default');
    } else {
      // if the video is only cued, I do not have to do anything to sync
      if (msg.playerState != 5) {
        player.seekTo(msg.playerTime, true);
      }
    }
  });

  session.hub.on('differentVideoLoaded', function (msg) {
    // load a new video if the host has loaded one
    var iframe = elementFinder.findElement(msg.element);
    var player = $(iframe).data("togetherjs-player");
    player.loadVideoById(msg.videoId, 0, 'default');
    $(iframe).data("currentVideoId", msg.videoId);

  });

  function synchronizeVideosOfLateGuest() {
    youTubeIframes.forEach(function (iframe) {
      var currentPlayer = $(iframe).data("togetherjs-player");
      var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
      var currentState = currentPlayer.getPlayerState();
      var currentTime = currentPlayer.getCurrentTime();
      var iframeLocation = elementFinder.elementLocation(iframe);
      session.send({
        type: "synchronizeVideosOfLateGuest",
        element: iframeLocation,
        videoId: currentVideoId,
        playerState: currentState, //this might be necessary later
        playerTime: currentTime
      });
    });
  }

  function isDifferentVideoLoaded(iframe) {
    var lastVideoId = $(iframe).data("currentVideoId");
    var currentPlayer = $(iframe).data("togetherjs-player");
    var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());

    // since url forms of iframe src and player's video url are different,
    // I have to compare the video ids
    if (currentVideoId != lastVideoId) {
      return currentVideoId;
    } else {
      return false;
    }
  }

  // parses videoId from the url returned by getVideoUrl function
  function getVideoIdFromUrl(videoUrl) {
    var videoId = videoUrl.split('v=')[1];
    //Chrome and Firefox have different positions for parameters
    var ampersandIndex = videoId.indexOf('&');
    if (ampersandIndex != -1) {
      videoId = videoId.substring(0, ampersandIndex);
    }
    return videoId;
  }

  function areTooFarApart(myTime, theirTime) {
    var secDiff = Math.abs(myTime - theirTime);
    var milliDiff = secDiff * 1000;
    return milliDiff > TOO_FAR_APART;
  }
});
<commit_msg>Handle iframes with no id<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http:// mozilla.org/MPL/2.0/. */

define(["jquery", "util", "session", "elementFinder"],
function ($, util, session, elementFinder) {

  // constant var to indicate whether two players are too far apart in sync
  var TOO_FAR_APART = 3000;
  // embedded youtube iframes
  var youTubeIframes = [];
  // youtube API load delay
  var API_LOADING_DELAY = 2000;

  session.on("reinitialize", function () {
    if (TogetherJS.config.get("youtube")) {
      prepareYouTube();
    }
  });

  session.on("close", function () {
    $(youTubeIframes).each(function (i, iframe) {
      // detach players from iframes
      $(iframe).removeData("togetherjs-player");
      $(iframe).removeData("dontPublish");
      $(iframe).removeData("currentVideoId");
      // disable iframeAPI
      $(iframe).removeAttr("enablejsapi");
      // remove unique youtube iframe indicators
      var id = $(iframe).attr("id") || "";
      if (id.indexOf("youtube-player") === 0) {
        // An id we added
        $(iframe).removeAttr("id");
      }
      youTubeIframes = [];
    });
  });

  TogetherJS.config.track("youtube", function (track, previous) {
    if (track && ! previous) {
      prepareYouTube();
      // You can enable youtube dynamically, but can't turn it off:
      TogetherJS.config.close("youtube");
    }
  });

  function prepareYouTube() {
    // setup iframes first
    setupYouTubeIframes();

    // this function should be global so it can be called when API is loaded
    window.onYouTubeIframeAPIReady = function() {
      // YouTube API is ready
      $(youTubeIframes).each(function (i, iframe) {
        var player = new YT.Player(iframe.id, { // get the reference to the already existing iframe
          events: {
            'onReady': insertPlayer,
            'onStateChange': publishPlayerStateChange
          }
        });
      });
    };

    if (window.YT === undefined) {
      // load necessary API
      // it calls onYouTubeIframeAPIReady automatically when the API finishes loading
      var tag = document.createElement('script');
      tag.src = "https://www.youtube.com/iframe_api";
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    } else {
      // manually invoke APIReady function when the API was already loaded by user
      onYouTubeIframeAPIReady();
    }

    // give each youtube iframe a unique id and set its enablejsapi param to true
    function setupYouTubeIframes() {
      var iframes = $('iframe');
      iframes.each(function (i, iframe) {
        // look for YouTube Iframes
        // if the iframe's unique id is already set, skip it
        if ($(iframe).attr("src").indexOf("youtube") != -1 && !$(iframe).attr("id")) {
          $(iframe).attr("id", "youtube-player"+i);
          $(iframe).attr("ensablejsapi", 1);
          youTubeIframes[i] = iframe;
        }
      });
    } // iframes are ready

    function insertPlayer(event) {
      // only when it is READY, attach a player to its iframe
      var currentPlayer = event.target;
      var currentIframe = currentPlayer.a;
      // check if a player is already attached in case of being reinitialized
      if (!$(currentIframe).data("togetherjs-player")) {
        $(currentIframe).data("togetherjs-player", currentPlayer);
        // initialize its dontPublish flag as well
        $(currentIframe).data("dontPublish", false);
        // store its current video's id
        var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
        $(currentIframe).data("currentVideoId", currentVideoId);
      }
    }
  } // end of prepareYouTube

  function publishPlayerStateChange(event) {
    var currentPlayer = event.target;
    var currentIframe = currentPlayer.a;
    var currentTime = currentPlayer.getCurrentTime();
    var iframeLocation = elementFinder.elementLocation(currentIframe);

    if ($(currentPlayer).data("seek")) {
      $(currentPlayer).removeData("seek");
      return;
    }

    // do not publish if playerState was changed by other users
    if ($(currentIframe).data("dontPublish")) {
      // make it false again so it can start publishing events of its own state changes
      $(currentIframe).data("dontPublish", false);
      return;
    }

    // notify other people that I changed the player state
    if (event.data == YT.PlayerState.PLAYING) {

      var currentVideoId = isDifferentVideoLoaded(currentIframe);
      if (currentVideoId) {
        // notify that I just loaded another video
        publishDifferentVideoLoaded(iframeLocation, currentVideoId);
        // update current video id
        $(currentIframe).data("currentVideoId", currentVideoId);
      } else {
        session.send({
          type: "playerStateChange",
          element: iframeLocation,
          playerState: 1,
          playerTime: currentTime
        });
      }
    } else if (event.data == YT.PlayerState.PAUSED) {
      session.send({
        type: "playerStateChange",
        element: iframeLocation,
        playerState: 2,
        playerTime: currentTime
      });
    } else {
      // do nothing when the state is buffering, cued, or ended
      return;
    }
  }

  function publishDifferentVideoLoaded(iframeLocation, videoId) {
    session.send({
      type: "differentVideoLoaded",
      videoId: videoId,
      element: iframeLocation
    });
  }

  session.hub.on('playerStateChange', function (msg) {
    var iframe = elementFinder.findElement(msg.element);
    var player = $(iframe).data("togetherjs-player");
    var currentTime = player.getCurrentTime();
    var currentState = player.getPlayerState();

    if (currentState != msg.playerState) {
      $(iframe).data("dontPublish", true);
    }

    if (msg.playerState == 1) {
      player.playVideo();
      // seekTo() updates the video's time and plays it if it was already playing
      // and pauses it if it was already paused
      if (areTooFarApart(currentTime, msg.playerTime)) {
        player.seekTo(msg.playerTime, true);
      }
    } else if (msg.playerState == 2) {
      // When YouTube videos are advanced while playing,
      // Chrome: pause -> pause -> play (onStateChange is called even when it is from pause to pause)
      // FireFox: buffering -> play -> buffering -> play
      // We must prevent advanced videos from going out of sync
      player.pauseVideo();
      if (areTooFarApart(currentTime, msg.playerTime)) {
        // "seek" flag will help supress publishing unwanted state changes
        $(player).data("seek", true);
        player.seekTo(msg.playerTime, true);
      }
    }
  });

  // if a late user joins a channel, synchronize his videos
  session.hub.on('hello', function () {
    // wait a couple seconds to make sure the late user has finished loading API
    setTimeout(synchronizeVideosOfLateGuest, API_LOADING_DELAY);
  });

  session.hub.on('synchronizeVideosOfLateGuest', function (msg) {
    var iframe = elementFinder.findElement(msg.element);
    var player = $(iframe).data("togetherjs-player");
    // check if another video had been loaded to an existing iframe before I joined
    var currentVideoId = $(iframe).data("currentVideoId");
    if (msg.videoId != currentVideoId) {
      $(iframe).data("currentVideoId", msg.videoId);
      player.loadVideoById(msg.videoId, msg.playerTime, 'default');
    } else {
      // if the video is only cued, I do not have to do anything to sync
      if (msg.playerState != 5) {
        player.seekTo(msg.playerTime, true);
      }
    }
  });

  session.hub.on('differentVideoLoaded', function (msg) {
    // load a new video if the host has loaded one
    var iframe = elementFinder.findElement(msg.element);
    var player = $(iframe).data("togetherjs-player");
    player.loadVideoById(msg.videoId, 0, 'default');
    $(iframe).data("currentVideoId", msg.videoId);

  });

  function synchronizeVideosOfLateGuest() {
    youTubeIframes.forEach(function (iframe) {
      var currentPlayer = $(iframe).data("togetherjs-player");
      var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());
      var currentState = currentPlayer.getPlayerState();
      var currentTime = currentPlayer.getCurrentTime();
      var iframeLocation = elementFinder.elementLocation(iframe);
      session.send({
        type: "synchronizeVideosOfLateGuest",
        element: iframeLocation,
        videoId: currentVideoId,
        playerState: currentState, //this might be necessary later
        playerTime: currentTime
      });
    });
  }

  function isDifferentVideoLoaded(iframe) {
    var lastVideoId = $(iframe).data("currentVideoId");
    var currentPlayer = $(iframe).data("togetherjs-player");
    var currentVideoId = getVideoIdFromUrl(currentPlayer.getVideoUrl());

    // since url forms of iframe src and player's video url are different,
    // I have to compare the video ids
    if (currentVideoId != lastVideoId) {
      return currentVideoId;
    } else {
      return false;
    }
  }

  // parses videoId from the url returned by getVideoUrl function
  function getVideoIdFromUrl(videoUrl) {
    var videoId = videoUrl.split('v=')[1];
    //Chrome and Firefox have different positions for parameters
    var ampersandIndex = videoId.indexOf('&');
    if (ampersandIndex != -1) {
      videoId = videoId.substring(0, ampersandIndex);
    }
    return videoId;
  }

  function areTooFarApart(myTime, theirTime) {
    var secDiff = Math.abs(myTime - theirTime);
    var milliDiff = secDiff * 1000;
    return milliDiff > TOO_FAR_APART;
  }
});
<|endoftext|>