<commit_before>'use strict';

const apiai = require('apiai');
const express = require('express');
const bodyParser = require('body-parser');
const uuid = require('uuid');
const request = require('request');
const JSONbig = require('json-bigint');
const async = require('async');

const REST_PORT = (process.env.PORT || 5000);
const APIAI_ACCESS_TOKEN = process.env.APIAI_ACCESS_TOKEN;
const APIAI_LANG = process.env.APIAI_LANG || 'en';
const FB_VERIFY_TOKEN = process.env.FB_VERIFY_TOKEN;
const FB_PAGE_ACCESS_TOKEN = process.env.FB_PAGE_ACCESS_TOKEN;
const FB_TEXT_LIMIT = 640;

const FACEBOOK_LOCATION = "FACEBOOK_LOCATION";
const FACEBOOK_WELCOME = "FACEBOOK_WELCOME";

class FacebookBot {
    constructor() {
        this.apiAiService = apiai(APIAI_ACCESS_TOKEN, {language: APIAI_LANG, requestSource: "fb"});
        this.sessionIds = new Map();
        this.messagesDelay = 200;
    }

    doDataResponse(sender, facebookResponseData) {
        if (!Array.isArray(facebookResponseData)) {
            console.log('Response as formatted message');
            this.sendFBMessage(sender, facebookResponseData)
                .catch(err => console.error(err));
        } else {
            async.eachSeries(facebookResponseData, (facebookMessage, callback) => {
                if (facebookMessage.sender_action) {
                    console.log('Response as sender action');
                    this.sendFBSenderAction(sender, facebookMessage.sender_action)
                        .then(() => callback())
                        .catch(err => callback(err));
                }
                else {
                    console.log('Response as formatted message');
                    this.sendFBMessage(sender, facebookMessage)
                        .then(() => callback())
                        .catch(err => callback(err));
                }
            }, (err) => {
                if (err) {
                    console.error(err);
                } else {
                    console.log('Data response completed');
                }
            });
        }
    }

    doRichContentResponse(sender, messages) {
        let facebookMessages = []; // array with result messages

        for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
            let message = messages[messageIndex];
			console.log('message.type: '+message.type);
            switch (message.type) {
                //message.type 0 means text message
                case 0:
                    // speech: ["hi"]
                    // we have to get value from fulfillment.speech, because of here is raw speech
                    if (message.speech) {

                        let splittedText = this.splitResponse(message.speech);

                        splittedText.forEach(s => {
                            facebookMessages.push({text: s});
                        });
                    }

                    break;
                //message.type 1 means card message
                case 1: {
                    let carousel = [message];

                    for (messageIndex++; messageIndex < messages.length; messageIndex++) {
                        if (messages[messageIndex].type == 1) {
                            carousel.push(messages[messageIndex]);
                        } else {
                            messageIndex--;
                            break;
                        }
                    }

                    let facebookMessage = {};
                    carousel.forEach((c) => {
                        // buttons: [ {text: "hi", postback: "postback"} ], imageUrl: "", title: "", subtitle: ""

                        let card = {};

                        card.title = c.title;
                        card.image_url = c.imageUrl;
                        if (this.isDefined(c.subtitle)) {
                            card.subtitle = c.subtitle;
                        }
                        //If button is involved in.
                        if (c.buttons.length > 0) {
                            let buttons = [];
                            for (let buttonIndex = 0; buttonIndex < c.buttons.length; buttonIndex++) {
                                let button = c.buttons[buttonIndex];

                                if (button.text) {
                                    let postback = button.postback;
                                    if (!postback) {
                                        postback = button.text;
                                    }

                                    let buttonDescription = {
                                        title: button.text
                                    };

                                    if (postback.startsWith("http")) {
                                        buttonDescription.type = "web_url";
                                        buttonDescription.url = postback;
                                    } else {
                                        buttonDescription.type = "postback";
                                        buttonDescription.payload = postback;
                                    }

                                    buttons.push(buttonDescription);
                                }
                            }

                            if (buttons.length > 0) {
                                card.buttons = buttons;
                            }
                        }

                        if (!facebookMessage.attachment) {
                            facebookMessage.attachment = {type: "template"};
                        }

                        if (!facebookMessage.attachment.payload) {
                            facebookMessage.attachment.payload = {template_type: "generic", elements: []};
                        }

                        facebookMessage.attachment.payload.elements.push(card);
                    });

                    facebookMessages.push(facebookMessage);
                }

                    break;
                //message.type 2 means quick replies message
                case 2: {
                    if (message.replies && message.replies.length > 0) {
                        let facebookMessage = {};

                        facebookMessage.text = message.title ? message.title : 'Choose an item';
                        facebookMessage.quick_replies = [];

                        message.replies.forEach((r) => {
                            facebookMessage.quick_replies.push({
                                content_type: "text",
                                title: r,
                                payload: r
                            });
                        });

                        facebookMessages.push(facebookMessage);
                    }
                }

                    break;
                //message.type 3 means image message
                case 3:

                    if (message.imageUrl) {
                        let facebookMessage = {};

                        // "imageUrl": "http://example.com/image.jpg"
                        facebookMessage.attachment = {type: "image"};
                        facebookMessage.attachment.payload = {url: message.imageUrl};
						console.log('message.imageUrl=' +message.imageUrl);
                        facebookMessages.push(facebookMessage);
                    }

                    break;
                //message.type 4 means custom payload message
                case 4:
                    if (message.payload && message.payload.facebook) {
                        facebookMessages.push(message.payload.facebook);
                    }
                    break;

                default:
                    break;
            }
        }

        return new Promise((resolve, reject) => {
            async.eachSeries(facebookMessages, (msg, callback) => {
                    this.sendFBSenderAction(sender, "typing_on")
                        .then(() => this.sleep(this.messagesDelay))
                        .then(() => this.sendFBMessage(sender, msg))
                        .then(() => callback())
                        .catch(callback);
                },
                (err) => {
                    if (err) {
                        console.error(err);
                        reject(err);
                    } else {
                        console.log('Messages sent');
                        resolve();
                    }
                });
        });

    }

    doTextResponse(sender, responseText) {
        console.log('Response as text message');
        // facebook API limit for text length is 640,
        // so we must split message if needed
        let splittedText = this.splitResponse(responseText);

        async.eachSeries(splittedText, (textPart, callback) => {
            this.sendFBMessage(sender, {text: textPart})
                .then(() => callback())
                .catch(err => callback(err));
        });
    }

    //which webhook event
    getEventText(event) {
        if (event.message) {
            if (event.message.quick_reply && event.message.quick_reply.payload) {
                console.log('event.message.quick_reply.payload = ' +event.message.quick_reply.payload);
				return event.message.quick_reply.payload;
            }

            if (event.message.text) {
				console.log('event.message.text = ' +event.message.text);
                return event.message.text;
            }
        }

        if (event.postback && event.postback.payload) {
            return event.postback.payload;
        }

        return null;

    }

    getFacebookEvent(event) {
        if (event.postback && event.postback.payload) {

            let payload = event.postback.payload;

            switch (payload) {
                case FACEBOOK_WELCOME:
                    return {name: FACEBOOK_WELCOME};

                case FACEBOOK_LOCATION:
                    return {name: FACEBOOK_LOCATION, data: event.postback.data}
            }
        }

        return null;
    }

    processFacebookEvent(event) {
        const sender = event.sender.id.toString();
        const eventObject = this.getFacebookEvent(event);

        if (eventObject) {

            // Handle a text message from this sender
            if (!this.sessionIds.has(sender)) {
                this.sessionIds.set(sender, uuid.v4());
            }

            let apiaiRequest = this.apiAiService.eventRequest(eventObject,
                {
                    sessionId: this.sessionIds.get(sender),
                    originalRequest: {
                        data: event,
                        source: "facebook"
                    }
                });
            this.doApiAiRequest(apiaiRequest, sender);
        }
    }

    processMessageEvent(event) {
		console.log('event.messageobj.type = ' +event.messageobj.type);
        const sender = event.sender.id.toString();
        const text = this.getEventText(event);

        if (text) {

            // Handle a text message from this sender
            if (!this.sessionIds.has(sender)) {
                this.sessionIds.set(sender, uuid.v4());
            }

            console.log("Text", text);
            //send user's text to api.ai service
            let apiaiRequest = this.apiAiService.textRequest(text,
                {
                    sessionId: this.sessionIds.get(sender),
                    originalRequest: {
                        data: event,
                        source: "facebook"
                    }
                });

            this.doApiAiRequest(apiaiRequest, sender);
        }
    }

    doApiAiRequest(apiaiRequest, sender) {
        apiaiRequest.on('response', (response) => {
            if (this.isDefined(response.result) && this.isDefined(response.result.fulfillment)) {
                let responseText = response.result.fulfillment.speech;
                let responseData = response.result.fulfillment.data;
                let responseMessages = response.result.fulfillment.messages;

                if (this.isDefined(responseData) && this.isDefined(responseData.facebook)) {
                    let facebookResponseData = responseData.facebook;
                    this.doDataResponse(sender, facebookResponseData);
                } else if (this.isDefined(responseMessages) && responseMessages.length > 0) {
                    this.doRichContentResponse(sender, responseMessages);
                }
                else if (this.isDefined(responseText)) {
                    this.doTextResponse(sender, responseText);
                }

            }
        });

        apiaiRequest.on('error', (error) => console.error(error));
        apiaiRequest.end();
    }

    splitResponse(str) {
        if (str.length <= FB_TEXT_LIMIT) {
            return [str];
        }

        return this.chunkString(str, FB_TEXT_LIMIT);
    }

    chunkString(s, len) {
        let curr = len, prev = 0;

        let output = [];

        while (s[curr]) {
            if (s[curr++] == ' ') {
                output.push(s.substring(prev, curr));
                prev = curr;
                curr += len;
            }
            else {
                let currReverse = curr;
                do {
                    if (s.substring(currReverse - 1, currReverse) == ' ') {
                        output.push(s.substring(prev, currReverse));
                        prev = currReverse;
                        curr = currReverse + len;
                        break;
                    }
                    currReverse--;
                } while (currReverse > prev)
            }
        }
        output.push(s.substr(prev));
        return output;
    }

    sendFBMessage(sender, messageData) {
        return new Promise((resolve, reject) => {
            request({
                url: 'https://graph.facebook.com/v2.6/me/messages',
                qs: {access_token: FB_PAGE_ACCESS_TOKEN},
                method: 'POST',
                json: {
                    recipient: {id: sender},
                    message: messageData
                }
            }, (error, response) => {
                if (error) {
                    console.log('Error sending message: ', error);
                    reject(error);
                } else if (response.body.error) {
                    console.log('Error: ', response.body.error);
                    reject(new Error(response.body.error));
                }

                resolve();
            });
        });
    }

    sendFBSenderAction(sender, action) {
        return new Promise((resolve, reject) => {
            request({
                url: 'https://graph.facebook.com/v2.6/me/messages',
                qs: {access_token: FB_PAGE_ACCESS_TOKEN},
                method: 'POST',
                json: {
                    recipient: {id: sender},
                    sender_action: action
                }
            }, (error, response) => {
                if (error) {
                    console.error('Error sending action: ', error);
                    reject(error);
                } else if (response.body.error) {
                    console.error('Error: ', response.body.error);
                    reject(new Error(response.body.error));
                }

                resolve();
            });
        });
    }

    doSubscribeRequest() {
        request({
                method: 'POST',
                uri: `https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=${FB_PAGE_ACCESS_TOKEN}`
            },
            (error, response, body) => {
                if (error) {
                    console.error('Error while subscription: ', error);
                } else {
                    console.log('Subscription result: ', response.body);
                }
            });
    }

    configureGetStartedEvent() {
        request({
                method: 'POST',
                uri: `https://graph.facebook.com/v2.6/me/thread_settings?access_token=${FB_PAGE_ACCESS_TOKEN}`,
                json: {
                    setting_type: "call_to_actions",
                    thread_state: "new_thread",
                    call_to_actions: [
                        {
                            payload: FACEBOOK_WELCOME
                        }
                    ]
                }
            },
            (error, response, body) => {
                if (error) {
                    console.error('Error while subscription', error);
                } else {
                    console.log('Subscription result', response.body);
                }
            });
    }

    isDefined(obj) {
        if (typeof obj == 'undefined') {
            return false;
        }

        if (!obj) {
            return false;
        }

        return obj != null;
    }

    sleep(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(), delay);
        });
    }

}

let facebookBot = new FacebookBot();

const app = express();

app.use(bodyParser.text({type: 'application/json'}));

app.get('/webhook/', (req, res) => {
    if (req.query['hub.verify_token'] === FB_VERIFY_TOKEN) {
        res.send(req.query['hub.challenge']);

        setTimeout(() => {
            facebookBot.doSubscribeRequest();
        }, 3000);
    } else {
        res.send('Error, wrong validation token');
    }
});

app.post('/webhook/', (req, res) => {
    try {
        const data = JSONbig.parse(req.body);

        if (data.entry) {
            let entries = data.entry;
            entries.forEach((entry) => {
                let messaging_events = entry.messaging;
                if (messaging_events) {
                    messaging_events.forEach((event) => {
                        if (event.message && !event.message.is_echo) {

                            if (event.message.attachments) {
                                let locations = event.message.attachments.filter(a => a.type === "location");

                                // delete all locations from original message
                                event.message.attachments = event.message.attachments.filter(a => a.type !== "location");

                                if (locations.length > 0) {
                                    locations.forEach(l => {
                                        let locationEvent = {
                                            sender: event.sender,
                                            postback: {
                                                payload: "FACEBOOK_LOCATION",
                                                data: l.payload.coordinates
                                            }
                                        };

                                        facebookBot.processFacebookEvent(locationEvent);
                                    });
                                }
                            }

                            facebookBot.processMessageEvent(event);
                        } else if (event.postback && event.postback.payload) {
                            if (event.postback.payload === "FACEBOOK_WELCOME") {
                                facebookBot.processFacebookEvent(event);
                            } else {
                                facebookBot.processMessageEvent(event);
                            }
                        }
                    });
                }
            });
        }

        return res.status(200).json({
            status: "ok"
        });
    } catch (err) {
        return res.status(400).json({
            status: "error",
            error: err
        });
    }

});

app.listen(REST_PORT, () => {
    console.log('Rest service ready on port ' + REST_PORT);
});

facebookBot.doSubscribeRequest();
<commit_msg>040817<commit_after>'use strict';

const apiai = require('apiai');
const express = require('express');
const bodyParser = require('body-parser');
const uuid = require('uuid');
const request = require('request');
const JSONbig = require('json-bigint');
const async = require('async');

const REST_PORT = (process.env.PORT || 5000);
const APIAI_ACCESS_TOKEN = process.env.APIAI_ACCESS_TOKEN;
const APIAI_LANG = process.env.APIAI_LANG || 'en';
const FB_VERIFY_TOKEN = process.env.FB_VERIFY_TOKEN;
const FB_PAGE_ACCESS_TOKEN = process.env.FB_PAGE_ACCESS_TOKEN;
const FB_TEXT_LIMIT = 640;

const FACEBOOK_LOCATION = "FACEBOOK_LOCATION";
const FACEBOOK_WELCOME = "FACEBOOK_WELCOME";

class FacebookBot {
    constructor() {
        this.apiAiService = apiai(APIAI_ACCESS_TOKEN, {language: APIAI_LANG, requestSource: "fb"});
        this.sessionIds = new Map();
        this.messagesDelay = 200;
    }

    doDataResponse(sender, facebookResponseData) {
        if (!Array.isArray(facebookResponseData)) {
            console.log('Response as formatted message');
            this.sendFBMessage(sender, facebookResponseData)
                .catch(err => console.error(err));
        } else {
            async.eachSeries(facebookResponseData, (facebookMessage, callback) => {
                if (facebookMessage.sender_action) {
                    console.log('Response as sender action');
                    this.sendFBSenderAction(sender, facebookMessage.sender_action)
                        .then(() => callback())
                        .catch(err => callback(err));
                }
                else {
                    console.log('Response as formatted message');
                    this.sendFBMessage(sender, facebookMessage)
                        .then(() => callback())
                        .catch(err => callback(err));
                }
            }, (err) => {
                if (err) {
                    console.error(err);
                } else {
                    console.log('Data response completed');
                }
            });
        }
    }

    doRichContentResponse(sender, messages) {
        let facebookMessages = []; // array with result messages

        for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
            let message = messages[messageIndex];
			console.log('message.type: '+message.type);
            switch (message.type) {
                //message.type 0 means text message
                case 0:
                    // speech: ["hi"]
                    // we have to get value from fulfillment.speech, because of here is raw speech
                    if (message.speech) {

                        let splittedText = this.splitResponse(message.speech);

                        splittedText.forEach(s => {
                            facebookMessages.push({text: s});
                        });
                    }

                    break;
                //message.type 1 means card message
                case 1: {
                    let carousel = [message];

                    for (messageIndex++; messageIndex < messages.length; messageIndex++) {
                        if (messages[messageIndex].type == 1) {
                            carousel.push(messages[messageIndex]);
                        } else {
                            messageIndex--;
                            break;
                        }
                    }

                    let facebookMessage = {};
                    carousel.forEach((c) => {
                        // buttons: [ {text: "hi", postback: "postback"} ], imageUrl: "", title: "", subtitle: ""

                        let card = {};

                        card.title = c.title;
                        card.image_url = c.imageUrl;
                        if (this.isDefined(c.subtitle)) {
                            card.subtitle = c.subtitle;
                        }
                        //If button is involved in.
                        if (c.buttons.length > 0) {
                            let buttons = [];
                            for (let buttonIndex = 0; buttonIndex < c.buttons.length; buttonIndex++) {
                                let button = c.buttons[buttonIndex];

                                if (button.text) {
                                    let postback = button.postback;
                                    if (!postback) {
                                        postback = button.text;
                                    }

                                    let buttonDescription = {
                                        title: button.text
                                    };

                                    if (postback.startsWith("http")) {
                                        buttonDescription.type = "web_url";
                                        buttonDescription.url = postback;
                                    } else {
                                        buttonDescription.type = "postback";
                                        buttonDescription.payload = postback;
                                    }

                                    buttons.push(buttonDescription);
                                }
                            }

                            if (buttons.length > 0) {
                                card.buttons = buttons;
                            }
                        }

                        if (!facebookMessage.attachment) {
                            facebookMessage.attachment = {type: "template"};
                        }

                        if (!facebookMessage.attachment.payload) {
                            facebookMessage.attachment.payload = {template_type: "generic", elements: []};
                        }

                        facebookMessage.attachment.payload.elements.push(card);
                    });

                    facebookMessages.push(facebookMessage);
                }

                    break;
                //message.type 2 means quick replies message
                case 2: {
                    if (message.replies && message.replies.length > 0) {
                        let facebookMessage = {};

                        facebookMessage.text = message.title ? message.title : 'Choose an item';
                        facebookMessage.quick_replies = [];

                        message.replies.forEach((r) => {
                            facebookMessage.quick_replies.push({
                                content_type: "text",
                                title: r,
                                payload: r
                            });
                        });

                        facebookMessages.push(facebookMessage);
                    }
                }

                    break;
                //message.type 3 means image message
                case 3:

                    if (message.imageUrl) {
                        let facebookMessage = {};

                        // "imageUrl": "http://example.com/image.jpg"
                        facebookMessage.attachment = {type: "image"};
                        facebookMessage.attachment.payload = {url: message.imageUrl};
						console.log('message.imageUrl=' +message.imageUrl);
                        facebookMessages.push(facebookMessage);
                    }

                    break;
                //message.type 4 means custom payload message
                case 4:
                    if (message.payload && message.payload.facebook) {
                        facebookMessages.push(message.payload.facebook);
                    }
                    break;

                default:
                    break;
            }
        }

        return new Promise((resolve, reject) => {
            async.eachSeries(facebookMessages, (msg, callback) => {
                    this.sendFBSenderAction(sender, "typing_on")
                        .then(() => this.sleep(this.messagesDelay))
                        .then(() => this.sendFBMessage(sender, msg))
                        .then(() => callback())
                        .catch(callback);
                },
                (err) => {
                    if (err) {
                        console.error(err);
                        reject(err);
                    } else {
                        console.log('Messages sent');
                        resolve();
                    }
                });
        });

    }

    doTextResponse(sender, responseText) {
        console.log('Response as text message');
        // facebook API limit for text length is 640,
        // so we must split message if needed
        let splittedText = this.splitResponse(responseText);

        async.eachSeries(splittedText, (textPart, callback) => {
            this.sendFBMessage(sender, {text: textPart})
                .then(() => callback())
                .catch(err => callback(err));
        });
    }

    //which webhook event
    getEventText(event) {
        if (event.message) {
            if (event.message.quick_reply && event.message.quick_reply.payload) {
                console.log('event.message.quick_reply.payload = ' +event.message.quick_reply.payload);
				return event.message.quick_reply.payload;
            }

            if (event.message.text) {
				console.log('event.message.text = ' +event.message.text);
                return event.message.text;
            }
        }

        if (event.postback && event.postback.payload) {
            return event.postback.payload;
        }

        return null;

    }

    getFacebookEvent(event) {
        if (event.postback && event.postback.payload) {

            let payload = event.postback.payload;

            switch (payload) {
                case FACEBOOK_WELCOME:
                    return {name: FACEBOOK_WELCOME};

                case FACEBOOK_LOCATION:
                    return {name: FACEBOOK_LOCATION, data: event.postback.data}
            }
        }

        return null;
    }

    processFacebookEvent(event) {
        const sender = event.sender.id.toString();
        const eventObject = this.getFacebookEvent(event);

        if (eventObject) {

            // Handle a text message from this sender
            if (!this.sessionIds.has(sender)) {
                this.sessionIds.set(sender, uuid.v4());
            }

            let apiaiRequest = this.apiAiService.eventRequest(eventObject,
                {
                    sessionId: this.sessionIds.get(sender),
                    originalRequest: {
                        data: event,
                        source: "facebook"
                    }
                });
            this.doApiAiRequest(apiaiRequest, sender);
        }
    }

    processMessageEvent(event) {
		console.log('inicio  processMessageEvent');
		if (event){
			console.log('event.messageobj.type= ' +event.messageobj.type);
		}
        const sender = event.sender.id.toString();
        const text = this.getEventText(event);

        if (text) {
			console.log("Text", text);
            // Handle a text message from this sender
            if (!this.sessionIds.has(sender)) {
                this.sessionIds.set(sender, uuid.v4());
            }

            //send user's text to api.ai service
            let apiaiRequest = this.apiAiService.textRequest(text,
                {
                    sessionId: this.sessionIds.get(sender),
                    originalRequest: {
                        data: event,
                        source: "facebook"
                    }
                });

            this.doApiAiRequest(apiaiRequest, sender);
        }
    }

    doApiAiRequest(apiaiRequest, sender) {
        apiaiRequest.on('response', (response) => {
            if (this.isDefined(response.result) && this.isDefined(response.result.fulfillment)) {
                let responseText = response.result.fulfillment.speech;
                let responseData = response.result.fulfillment.data;
                let responseMessages = response.result.fulfillment.messages;

                if (this.isDefined(responseData) && this.isDefined(responseData.facebook)) {
                    let facebookResponseData = responseData.facebook;
                    this.doDataResponse(sender, facebookResponseData);
                } else if (this.isDefined(responseMessages) && responseMessages.length > 0) {
                    this.doRichContentResponse(sender, responseMessages);
                }
                else if (this.isDefined(responseText)) {
                    this.doTextResponse(sender, responseText);
                }

            }
        });

        apiaiRequest.on('error', (error) => console.error(error));
        apiaiRequest.end();
    }

    splitResponse(str) {
        if (str.length <= FB_TEXT_LIMIT) {
            return [str];
        }

        return this.chunkString(str, FB_TEXT_LIMIT);
    }

    chunkString(s, len) {
        let curr = len, prev = 0;

        let output = [];

        while (s[curr]) {
            if (s[curr++] == ' ') {
                output.push(s.substring(prev, curr));
                prev = curr;
                curr += len;
            }
            else {
                let currReverse = curr;
                do {
                    if (s.substring(currReverse - 1, currReverse) == ' ') {
                        output.push(s.substring(prev, currReverse));
                        prev = currReverse;
                        curr = currReverse + len;
                        break;
                    }
                    currReverse--;
                } while (currReverse > prev)
            }
        }
        output.push(s.substr(prev));
        return output;
    }

    sendFBMessage(sender, messageData) {
        return new Promise((resolve, reject) => {
            request({
                url: 'https://graph.facebook.com/v2.6/me/messages',
                qs: {access_token: FB_PAGE_ACCESS_TOKEN},
                method: 'POST',
                json: {
                    recipient: {id: sender},
                    message: messageData
                }
            }, (error, response) => {
                if (error) {
                    console.log('Error sending message: ', error);
                    reject(error);
                } else if (response.body.error) {
                    console.log('Error: ', response.body.error);
                    reject(new Error(response.body.error));
                }

                resolve();
            });
        });
    }

    sendFBSenderAction(sender, action) {
        return new Promise((resolve, reject) => {
            request({
                url: 'https://graph.facebook.com/v2.6/me/messages',
                qs: {access_token: FB_PAGE_ACCESS_TOKEN},
                method: 'POST',
                json: {
                    recipient: {id: sender},
                    sender_action: action
                }
            }, (error, response) => {
                if (error) {
                    console.error('Error sending action: ', error);
                    reject(error);
                } else if (response.body.error) {
                    console.error('Error: ', response.body.error);
                    reject(new Error(response.body.error));
                }

                resolve();
            });
        });
    }

    doSubscribeRequest() {
        request({
                method: 'POST',
                uri: `https://graph.facebook.com/v2.6/me/subscribed_apps?access_token=${FB_PAGE_ACCESS_TOKEN}`
            },
            (error, response, body) => {
                if (error) {
                    console.error('Error while subscription: ', error);
                } else {
                    console.log('Subscription result: ', response.body);
                }
            });
    }

    configureGetStartedEvent() {
        request({
                method: 'POST',
                uri: `https://graph.facebook.com/v2.6/me/thread_settings?access_token=${FB_PAGE_ACCESS_TOKEN}`,
                json: {
                    setting_type: "call_to_actions",
                    thread_state: "new_thread",
                    call_to_actions: [
                        {
                            payload: FACEBOOK_WELCOME
                        }
                    ]
                }
            },
            (error, response, body) => {
                if (error) {
                    console.error('Error while subscription', error);
                } else {
                    console.log('Subscription result', response.body);
                }
            });
    }

    isDefined(obj) {
        if (typeof obj == 'undefined') {
            return false;
        }

        if (!obj) {
            return false;
        }

        return obj != null;
    }

    sleep(delay) {
        return new Promise((resolve, reject) => {
            setTimeout(() => resolve(), delay);
        });
    }

}

let facebookBot = new FacebookBot();

const app = express();

app.use(bodyParser.text({type: 'application/json'}));

app.get('/webhook/', (req, res) => {
    if (req.query['hub.verify_token'] === FB_VERIFY_TOKEN) {
        res.send(req.query['hub.challenge']);

        setTimeout(() => {
            facebookBot.doSubscribeRequest();
        }, 3000);
    } else {
        res.send('Error, wrong validation token');
    }
});

app.post('/webhook/', (req, res) => {
    try {
        const data = JSONbig.parse(req.body);

        if (data.entry) {
            let entries = data.entry;
            entries.forEach((entry) => {
                let messaging_events = entry.messaging;
                if (messaging_events) {
                    messaging_events.forEach((event) => {
                        if (event.message && !event.message.is_echo) {

                            if (event.message.attachments) {
                                let locations = event.message.attachments.filter(a => a.type === "location");

                                // delete all locations from original message
                                event.message.attachments = event.message.attachments.filter(a => a.type !== "location");

                                if (locations.length > 0) {
                                    locations.forEach(l => {
                                        let locationEvent = {
                                            sender: event.sender,
                                            postback: {
                                                payload: "FACEBOOK_LOCATION",
                                                data: l.payload.coordinates
                                            }
                                        };

                                        facebookBot.processFacebookEvent(locationEvent);
                                    });
                                }
                            }

                            facebookBot.processMessageEvent(event);
                        } else if (event.postback && event.postback.payload) {
                            if (event.postback.payload === "FACEBOOK_WELCOME") {
                                facebookBot.processFacebookEvent(event);
                            } else {
                                facebookBot.processMessageEvent(event);
                            }
                        }
                    });
                }
            });
        }

        return res.status(200).json({
            status: "ok"
        });
    } catch (err) {
        return res.status(400).json({
            status: "error",
            error: err
        });
    }

});

app.listen(REST_PORT, () => {
    console.log('Rest service ready on port ' + REST_PORT);
});

facebookBot.doSubscribeRequest();
<|endoftext|>


<commit_before>'use strict';

import debugFactory from "debug";
const debug = debugFactory('app:common:util');

//obviously, these should eventually be read from the data so it isn't hard-limited
const minYear = 2000;
const maxYear = 2015;
const months = {
    'JanPrec': 1,
    'FebPrec': 2,
    'MarPrec': 3,
    'AprPrec': 4,
    'MayPrec': 5,
    'JunPrec': 6,
    'JulPrec': 7,
    'AugPrec': 8,
    'SepPrec': 9,
    'OctPrec': 10,
    'NovPrec': 11,
    'DecPrec': 12
};

const deg2rad = (deg) =>deg * Math.PI / 180; // radians = degrees * pi/180

module.exports = {
    reducers: {
        average: {
            add(key) {
                return function (p, v) {
                    ++p.count;
                    p.sum += (v[key] || 0);
                    p.avg = Math.round(p.sum / p.count);
                    return p;
                };
            },
            remove(key) {
                return function (p, v) {
                    --p.count;
                    p.sum -= (v[key] || 0);
                    p.avg = p.count ? Math.round(p.sum / p.count) : 0;
                    return p;
                };
            },
            init() {
                return function () {
                    return {count: 0, sum: 0, avg: 0};
                };
            }
        },
        location: {
            add() {
                return function (p, v) {
                    ++p.count;
                    p.latitude = v.location.latitude;
                    p.longitude = v.location.longitude;
                    p.label = v.name; //last one wins
                    return p;
                };
            },
            remove() {
                return function (p, v) {
                    --p.count;
                    return p;
                };
            },
            init() {
                return function () {
                    return {count: 0};
                };
            }
        }
    },
    rainfall: {
        monthly(data) {
            const output = [];
            data.forEach((d) =>{
                //push a set of items in for every month in the row
                Object.keys(months).forEach((month) => {
                    output.push({
                        id: d.id1,
                        date: new Date(d.Year * 1, months[month], 1), //first day of the month
                        value: d[month] * 1 //coerce precip for that month
                    });
                });
            });
            return output;
        },

        yearly(data) {
            const output = [];
            data.forEach((d) =>{
                //push a set of items in for every month in the row
                let totalRainInYear = 0;
                Object.keys(months).forEach((month) => totalRainInYear += parseFloat(d[month]));
                output.push({
                    id: d.id1,
                    date: new Date(d.Year * 1, 1, 1), //first day of the year
                    value: totalRainInYear
                });
            });
            return output;
        },

        //this function does the same as monthly, but duplicates the 30-year average '0000' readings for each year in the dataset
        monthlyAverage30(data) {
            const output = [];
            data.forEach((d) => {
                //push a set of items in for every month in the row
                Object.keys(months).forEach((month) => {
                    //replicate the data for each month for all years - this is how we compare to the regular monthly data
                    const monthNum = months[month];
                    const value = d[month] * 1;  //coerce precip for that month
                    for (let year = minYear; year <= maxYear; year++) {
                        output.push({
                            id: d.id1,
                            date: new Date(year, monthNum, 1), //first day of the month
                            value: value
                        });
                    }
                });
            });
            return output;
        }
    },
    geospatial: {
        deg2rad,
        hitTestPoints(points, center, radius) {
            const output = {};
            //uses Great Circle distance to check if point diff is within radius
            points.forEach((point) => {
                //http://andrew.hedges.name/experiments/haversine/
                //dlon = lon2 - lon1
                //dlat = lat2 - lat1
                //a = (sin(dlat/2))^2 + cos(lat1) * cos(lat2) * (sin(dlon/2))^2
                //c = 2 * atan2( sqrt(a), sqrt(1-a) )
                //d = R * c (where R is the radius of the Earth)

                const lat1 = deg2rad(center.lat);
                const lat2 = deg2rad(point.lat);
                const lon1 = deg2rad(center.lng);
                const lon2 = deg2rad(point.lon);
                const dlon = lon2 - lon1;
                const dlat = lat2 - lat1;
                const a = Math.pow(Math.sin(dlat / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon / 2), 2);
                const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                const d = 3961 * c; //radius of earth in miles
                if (d < radius) {
                    output[point.id] = point;
                }
            });
            debug(`${points.length} subset count => ${Object.keys(output).length}`);
            return output;
        }
    }
};<commit_msg>Ignore NaN Values Instead of Coalescing Them to 0 in AveragingReducer<commit_after>'use strict';

import debugFactory from "debug";
const debug = debugFactory('app:common:util');

//obviously, these should eventually be read from the data so it isn't hard-limited
const minYear = 2000;
const maxYear = 2015;
const months = {
    'JanPrec': 1,
    'FebPrec': 2,
    'MarPrec': 3,
    'AprPrec': 4,
    'MayPrec': 5,
    'JunPrec': 6,
    'JulPrec': 7,
    'AugPrec': 8,
    'SepPrec': 9,
    'OctPrec': 10,
    'NovPrec': 11,
    'DecPrec': 12
};

const deg2rad = (deg) =>deg * Math.PI / 180; // radians = degrees * pi/180

module.exports = {
    reducers: {
        average: {
            add(key) {
                return function (p, v) {
                    if (!isNaN(v[key])) {
                        ++p.count;
                        p.sum += v[key];
                        p.avg = Math.round(p.sum / p.count);
                    }
                    return p;
                };
            },
            remove(key) {
                return function (p, v) {
                    if (!isNaN(v[key])) {
                        --p.count;
                        p.sum -= v[key];
                        p.avg = p.count ? Math.round(p.sum / p.count) : 0;
                    }
                    return p;
                };
            },
            init() {
                return function () {
                    return {count: 0, sum: 0, avg: 0};
                };
            }
        },
        location: {
            add() {
                return function (p, v) {
                    ++p.count;
                    p.latitude = v.location.latitude;
                    p.longitude = v.location.longitude;
                    p.label = v.name; //last one wins
                    return p;
                };
            },
            remove() {
                return function (p, v) {
                    --p.count;
                    return p;
                };
            },
            init() {
                return function () {
                    return {count: 0};
                };
            }
        }
    },
    rainfall: {
        monthly(data) {
            const output = [];
            data.forEach((d) =>{
                //push a set of items in for every month in the row
                Object.keys(months).forEach((month) => {
                    output.push({
                        id: d.id1,
                        date: new Date(d.Year * 1, months[month], 1), //first day of the month
                        value: d[month] * 1 //coerce precip for that month
                    });
                });
            });
            return output;
        },

        yearly(data) {
            const output = [];
            data.forEach((d) =>{
                //push a set of items in for every month in the row
                let totalRainInYear = 0;
                Object.keys(months).forEach((month) => totalRainInYear += parseFloat(d[month]));
                output.push({
                    id: d.id1,
                    date: new Date(d.Year * 1, 1, 1), //first day of the year
                    value: totalRainInYear
                });
            });
            return output;
        },

        //this function does the same as monthly, but duplicates the 30-year average '0000' readings for each year in the dataset
        monthlyAverage30(data) {
            const output = [];
            data.forEach((d) => {
                //push a set of items in for every month in the row
                Object.keys(months).forEach((month) => {
                    //replicate the data for each month for all years - this is how we compare to the regular monthly data
                    const monthNum = months[month];
                    const value = d[month] * 1;  //coerce precip for that month
                    for (let year = minYear; year <= maxYear; year++) {
                        output.push({
                            id: d.id1,
                            date: new Date(year, monthNum, 1), //first day of the month
                            value: value
                        });
                    }
                });
            });
            return output;
        }
    },
    geospatial: {
        deg2rad,
        hitTestPoints(points, center, radius) {
            const output = {};
            //uses Great Circle distance to check if point diff is within radius
            points.forEach((point) => {
                //http://andrew.hedges.name/experiments/haversine/
                //dlon = lon2 - lon1
                //dlat = lat2 - lat1
                //a = (sin(dlat/2))^2 + cos(lat1) * cos(lat2) * (sin(dlon/2))^2
                //c = 2 * atan2( sqrt(a), sqrt(1-a) )
                //d = R * c (where R is the radius of the Earth)

                const lat1 = deg2rad(center.lat);
                const lat2 = deg2rad(point.lat);
                const lon1 = deg2rad(center.lng);
                const lon2 = deg2rad(point.lon);
                const dlon = lon2 - lon1;
                const dlat = lat2 - lat1;
                const a = Math.pow(Math.sin(dlat / 2), 2) + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dlon / 2), 2);
                const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
                const d = 3961 * c; //radius of earth in miles
                if (d < radius) {
                    output[point.id] = point;
                }
            });
            debug(`${points.length} subset count => ${Object.keys(output).length}`);
            return output;
        }
    }
};<|endoftext|>


<commit_before>// ------------------------------------------------------------------------
// GMaps: search.js
// ------------------------------------------------------------------------

!((Core, Util) => {
  "use strict"

  // ----------------------------------------------------------------------
  // Public Functions
  // ----------------------------------------------------------------------

  Core.search = function(map, type, ids) {
    const compArray = map.Components[type]
    if (ids) {
      const exclude = _getIdsToExclude(compArray, Util.toArray(ids))
      return Util.copy(compArray, exclude)
    }
    return compArray
  }

  // ----------------------------------------------------------------------
  // Private Functions
  // ----------------------------------------------------------------------

  function _getIdsToExclude(compArray, ids) {
    const allIDs = compArray.getIds()
    const exclude = allIDs.filter(function(i) {
      return ids.indexOf(i) === -1
    })

    return exclude
  }

  return Core
})(gmap.Core || (gmap.Core = {}), gmap.Util)
<commit_msg>patched Core.Search<commit_after>// ------------------------------------------------------------------------
// GMaps: search.js
// ------------------------------------------------------------------------

!((Core, Util) => {
  "use strict"

  // ----------------------------------------------------------------------
  // Public Functions
  // ----------------------------------------------------------------------

  Core.search = function(map, type, ids) {
    const compArray = map.Components[type]
    if (ids) {
      const exclude = _getIdsToExclude(compArray, Util.toArray(ids))
      return Util.copy(compArray, exclude)
    }
    return compArray
  }

  // ----------------------------------------------------------------------
  // Private Functions
  // ----------------------------------------------------------------------

  function _getIdsToExclude(compArray, ids) {
    // ensure ids are strings
    ids = ids.toString().split(",")

    const allIDs = compArray.getIds()
    const exclude = allIDs.filter(function(i) {
      return ids.indexOf(i) === -1
    })
    return exclude
  }

  return Core
})(gmap.Core || (gmap.Core = {}), gmap.Util)
<|endoftext|>


<commit_before>'use strict';

// render the formBuilder XML into html
function FormRenderFn(options, element) {

  var utils = fbUtils;

  var formRender = this,
    defaults = {
      destroyTemplate: true, // @todo
      container: false,
      dataType: 'xml',
      formData: false,
      label: {
        formRendered: 'Form Rendered',
        noFormData: 'No form data.',
        other: 'Other',
        selectColor: 'Select Color'
      },
      render: true,
      notify: {
        error: function(message) {
          return console.error(message);
        },
        success: function(message) {
          return console.log(message);
        },
        warning: function(message) {
          return console.warn(message);
        }
      }
    },
    _helpers = {};

  var opts = $.extend(true, defaults, options);

  (function() {
    if (!opts.formData) {
      return false;
    }

    let setData = {
      xml: formData => utils.parseXML(formData),
      json: formData => window.JSON.parse(formData)
    };

    opts.formData = setData[opts.dataType](opts.formData) || false;
  })();

  /**
   * Generate preview markup
   * @param  {object} fieldData
   * @return {string}       preview markup for field
   */
  _helpers.fieldRender = function(fieldData) {
    var fieldMarkup = '',
      fieldLabel = '',
      optionsMarkup = '',
      fieldLabelText = fieldData.label || '',
      fieldDesc = fieldData.description || '',
      fieldRequired = '',
      fieldOptions = fieldData.values || [];
    fieldData.id = fieldData.name;

    fieldData.type = fieldData.subtype || fieldData.type;

    if (fieldData.required) {
      fieldData.required = null;
      fieldData['aria-required'] = 'true';
      fieldRequired = `<span class="required">*</span>`;
    }

    if (fieldData.type !== 'hidden') {
      if (fieldDesc) {
        fieldDesc = `<span class="tooltip-element" tooltip="${fieldDesc}">?</span>`;
      }
      fieldLabel = `<label for="${fieldData.id}">${fieldLabelText} ${fieldRequired} ${fieldDesc}</label>`;
    }

    var fieldLabelVal = fieldData.label;

    delete fieldData.label;
    delete fieldData.description;

    var fieldDataString = utils.attrString(fieldData);

    switch (fieldData.type) {
      case 'textarea':
      case 'rich-text':
        delete fieldData.type;
        let fieldVal = fieldData.value || '';
        fieldMarkup = `${fieldLabel}<textarea ${fieldDataString}>${fieldVal}</textarea>`;
        break;
      case 'select':
        var optionAttrsString;
        fieldData.type = fieldData.type.replace('-group', '');

        if (fieldOptions) {
          for (let i = 0; i < fieldOptions.length; i++) {
            if (!fieldOptions[i].selected) {
              delete fieldOptions[i].selected;
            }
            optionAttrsString = utils.attrString(fieldOptions[i]);
            optionsMarkup += `<option ${optionAttrsString}>${fieldOptions[i].label}</option>`;
          }
        }

        fieldMarkup = `${fieldLabel}<select ${fieldDataString}>${optionsMarkup}</select>`;
        break;
      case 'checkbox-group':
      case 'radio-group':
        let optionAttrs;
        fieldData.type = fieldData.type.replace('-group', '');

        if (fieldOptions) {
          let optionName = fieldData.type === 'checkbox' ? fieldData.name + '[]' : fieldData.name,
            optionAttrsString;

          for (let i = 0; i < fieldOptions.length; i++) {
            optionAttrs = Object.assign({}, fieldData, fieldOptions[i]);

            if (optionAttrs.selected) {
              delete optionAttrs.selected;
              optionAttrs.checked = null;
            }

            optionAttrs.name = optionName;
            optionAttrs.id = fieldData.id + '-' + i;
            optionAttrsString = utils.attrString(optionAttrs);
            optionsMarkup += `<input ${optionAttrsString} /> <label for="${optionAttrs.id}">${optionAttrs.label}</label><br>`;
          }

          if (fieldData.enableOther || fieldData['enable-other']) {
            let otherOptionAttrs = {
              id: fieldData.id + '-' + 'other',
              className: (fieldData.class || fieldData.className) + ' other-option'
            };

            optionAttrsString = utils.attrString(Object.assign({}, fieldData, otherOptionAttrs));

            optionsMarkup += `<input ${optionAttrsString} /> ${opts.label.other} <input type="text" data-other-id="${otherOptionAttrs.id}" name="${optionName}" id="${otherOptionAttrs.id}-value" style="display:none;" />`;
          }

        }
        fieldMarkup = `${fieldLabel}<div class="${fieldData.type}-group">${optionsMarkup}</div>`;
        break;
      case 'text':
      case 'password':
      case 'email':
      case 'number':
      case 'file':
      case 'hidden':
      case 'date':
      case 'tel':
      case 'autocomplete':
        fieldMarkup = `${fieldLabel} <input ${fieldDataString}>`;
        break;
      case 'color':
        fieldMarkup = `${fieldLabel} <input ${fieldDataString}> ${opts.label.selectColor}`;
        break;
      case 'button':
      case 'submit':
        fieldMarkup = `<button ${fieldDataString}>${fieldLabelVal}</button>`;
        break;
      case 'checkbox':
        fieldMarkup = `<input ${fieldDataString}> ${fieldLabel}`;

        if (fieldData.toggle) {
          setTimeout(function() {
            $(document.getElementById(fieldData.id)).kcToggle();
          }, 100);
        }
        break;
      default:
        fieldMarkup = `<${fieldData.type} ${fieldDataString}>${fieldLabelVal}</${fieldData.type}>`;
    }

    if (fieldData.type !== 'hidden') {
      let className = fieldData.id ? 'form-group field-' + fieldData.id : '';
      fieldMarkup = utils.markup('div', fieldMarkup, {
        className: className
      });
    } else {
      fieldMarkup = utils.markup('input', null, fieldData);
    }

    return fieldMarkup;
  };

  /**
   * Extend Element prototype to allow us to append fields
   *
   * @param  {object} fields Node elements
   */
  Element.prototype.appendFormFields = function(fields) {
    var element = this;
    fields.reverse();
    for (var i = fields.length - 1; i >= 0; i--) {
      element.appendChild(fields[i]);
    }
  };

  /**
   * Extend Element prototype to remove content
   */
  Element.prototype.emptyContainer = function() {
    var element = this;
    while (element.lastChild) {
      element.removeChild(element.lastChild);
    }
  };

  var otherOptionCB = function() {
    var otherOptions = document.getElementsByClassName('other-option');
    for (var i = 0; i < otherOptions.length; i++) {
      let otherInput = document.getElementById(otherOptions[i].id + '-value');
      otherOptions[i].onclick = function() {
        let option = this;
        if (this.checked) {
          otherInput.style.display = 'inline-block';
          option.nextElementSibling.style.display = 'none';
          otherInput.oninput = function() { option.value = this.value; };
        } else {
          otherInput.style.display = 'none';
          option.nextElementSibling.style.display = 'inline-block';
          otherInput.oninput = undefined;
        }
      };
    }
  };

  var runCallbacks = function() {
    otherOptionCB();
  };

  // Begin the core plugin
  var rendered = [];

  // generate field markup if we have fields
  if (opts.formData) {
    for (var i = 0; i < opts.formData.length; i++) {
      rendered.push(_helpers.fieldRender(opts.formData[i]));
    }

    if (opts.render) {
      if (opts.container) {
        let renderedFormWrap = utils.markup('div', rendered, { className: 'rendered-form' });
        opts.container = (opts.container instanceof jQuery) ? opts.container[0] : opts.container;
        opts.container.emptyContainer();
        opts.container.appendChild(renderedFormWrap);
      } else if (element) {
        element.emptyContainer();
        element.appendFormFields(rendered);
      }

      runCallbacks();
      opts.notify.success(opts.label.formRendered);
    } else {
      formRender.markup = rendered.map(function(elem) {
        return elem.innerHTML;
      }).join('');
    }
  } else {
    let noData = utils.markup('div', opts.label.noFormData, {
      className: 'no-form-data'
    });
    rendered.push(noData);
    opts.notify.error(opts.label.noFormData);
  }

  return formRender;
}

(function($) {

  $.fn.formRender = function(options) {
    this.each(function() {
      let formRender = new FormRenderFn(options, this);
      return formRender;
    });
  };

})(jQuery);
<commit_msg>Bugfix: Checkbox/Radio other option does not work with XML dataType (#262)<commit_after>'use strict';

// render the formBuilder XML into html
function FormRenderFn(options, element) {

  var utils = fbUtils;

  var formRender = this,
    defaults = {
      destroyTemplate: true, // @todo
      container: false,
      dataType: 'xml',
      formData: false,
      label: {
        formRendered: 'Form Rendered',
        noFormData: 'No form data.',
        other: 'Other',
        selectColor: 'Select Color'
      },
      render: true,
      notify: {
        error: function(message) {
          return console.error(message);
        },
        success: function(message) {
          return console.log(message);
        },
        warning: function(message) {
          return console.warn(message);
        }
      }
    },
    _helpers = {};

  var opts = $.extend(true, defaults, options);

  (function() {
    if (!opts.formData) {
      return false;
    }

    let setData = {
      xml: formData => utils.parseXML(formData),
      json: formData => window.JSON.parse(formData)
    };

    opts.formData = setData[opts.dataType](opts.formData) || false;
  })();

  /**
   * Generate preview markup
   * @param  {object} fieldData
   * @return {string}       preview markup for field
   */
  _helpers.fieldRender = function(fieldData) {
    var fieldMarkup = '',
      fieldLabel = '',
      optionsMarkup = '',
      fieldLabelText = fieldData.label || '',
      fieldDesc = fieldData.description || '',
      fieldRequired = '',
      fieldOptions = fieldData.values || [];
    fieldData.id = fieldData.name;

    fieldData.type = fieldData.subtype || fieldData.type;

    if (fieldData.required) {
      fieldData.required = null;
      fieldData['aria-required'] = 'true';
      fieldRequired = `<span class="required">*</span>`;
    }

    if (fieldData.type !== 'hidden') {
      if (fieldDesc) {
        fieldDesc = `<span class="tooltip-element" tooltip="${fieldDesc}">?</span>`;
      }
      fieldLabel = `<label for="${fieldData.id}">${fieldLabelText} ${fieldRequired} ${fieldDesc}</label>`;
    }

    var fieldLabelVal = fieldData.label;

    delete fieldData.label;
    delete fieldData.description;

    var fieldDataString = utils.attrString(fieldData);

    switch (fieldData.type) {
      case 'textarea':
      case 'rich-text':
        delete fieldData.type;
        let fieldVal = fieldData.value || '';
        fieldMarkup = `${fieldLabel}<textarea ${fieldDataString}>${fieldVal}</textarea>`;
        break;
      case 'select':
        var optionAttrsString;
        fieldData.type = fieldData.type.replace('-group', '');

        if (fieldOptions) {
          for (let i = 0; i < fieldOptions.length; i++) {
            if (!fieldOptions[i].selected) {
              delete fieldOptions[i].selected;
            }
            optionAttrsString = utils.attrString(fieldOptions[i]);
            optionsMarkup += `<option ${optionAttrsString}>${fieldOptions[i].label}</option>`;
          }
        }

        fieldMarkup = `${fieldLabel}<select ${fieldDataString}>${optionsMarkup}</select>`;
        break;
      case 'checkbox-group':
      case 'radio-group':
        let optionAttrs;
        fieldData.type = fieldData.type.replace('-group', '');

        if (fieldOptions) {
          let optionName = fieldData.type === 'checkbox' ? fieldData.name + '[]' : fieldData.name,
            optionAttrsString;

          for (let i = 0; i < fieldOptions.length; i++) {
            optionAttrs = Object.assign({}, fieldData, fieldOptions[i]);

            if (optionAttrs.selected) {
              delete optionAttrs.selected;
              optionAttrs.checked = null;
            }

            optionAttrs.name = optionName;
            optionAttrs.id = fieldData.id + '-' + i;
            optionAttrsString = utils.attrString(optionAttrs);
            optionsMarkup += `<input ${optionAttrsString} /> <label for="${optionAttrs.id}">${optionAttrs.label}</label><br>`;
          }

          if (fieldData.enableOther || fieldData['enable-other']) {
            let otherOptionAttrs = {
              id: fieldData.id + '-' + 'other',
              className: fieldData.className + ' other-option'
            };

            optionAttrsString = utils.attrString(Object.assign({}, fieldData, otherOptionAttrs));

            optionsMarkup += `<input ${optionAttrsString} /> ${opts.label.other} <input type="text" data-other-id="${otherOptionAttrs.id}" name="${optionName}" id="${otherOptionAttrs.id}-value" style="display:none;" />`;
          }

        }
        fieldMarkup = `${fieldLabel}<div class="${fieldData.type}-group">${optionsMarkup}</div>`;
        break;
      case 'text':
      case 'password':
      case 'email':
      case 'number':
      case 'file':
      case 'hidden':
      case 'date':
      case 'tel':
      case 'autocomplete':
        fieldMarkup = `${fieldLabel} <input ${fieldDataString}>`;
        break;
      case 'color':
        fieldMarkup = `${fieldLabel} <input ${fieldDataString}> ${opts.label.selectColor}`;
        break;
      case 'button':
      case 'submit':
        fieldMarkup = `<button ${fieldDataString}>${fieldLabelVal}</button>`;
        break;
      case 'checkbox':
        fieldMarkup = `<input ${fieldDataString}> ${fieldLabel}`;

        if (fieldData.toggle) {
          setTimeout(function() {
            $(document.getElementById(fieldData.id)).kcToggle();
          }, 100);
        }
        break;
      default:
        fieldMarkup = `<${fieldData.type} ${fieldDataString}>${fieldLabelVal}</${fieldData.type}>`;
    }

    if (fieldData.type !== 'hidden') {
      let className = fieldData.id ? 'form-group field-' + fieldData.id : '';
      fieldMarkup = utils.markup('div', fieldMarkup, {
        className: className
      });
    } else {
      fieldMarkup = utils.markup('input', null, fieldData);
    }

    return fieldMarkup;
  };

  /**
   * Extend Element prototype to allow us to append fields
   *
   * @param  {object} fields Node elements
   */
  Element.prototype.appendFormFields = function(fields) {
    var element = this;
    fields.reverse();
    for (var i = fields.length - 1; i >= 0; i--) {
      element.appendChild(fields[i]);
    }
  };

  /**
   * Extend Element prototype to remove content
   */
  Element.prototype.emptyContainer = function() {
    var element = this;
    while (element.lastChild) {
      element.removeChild(element.lastChild);
    }
  };

  var otherOptionCB = function() {
    var otherOptions = document.getElementsByClassName('other-option');
    for (var i = 0; i < otherOptions.length; i++) {
      let otherInput = document.getElementById(otherOptions[i].id + '-value');
      otherOptions[i].onclick = function() {
        let option = this;
        if (this.checked) {
          otherInput.style.display = 'inline-block';
          option.nextElementSibling.style.display = 'none';
          otherInput.oninput = function() { option.value = this.value; };
        } else {
          otherInput.style.display = 'none';
          option.nextElementSibling.style.display = 'inline-block';
          otherInput.oninput = undefined;
        }
      };
    }
  };

  var runCallbacks = function() {
    otherOptionCB();
  };

  var santizeField = (field) => {
    let sanitizedField = Object.assign({}, field);
    sanitizedField.className = field.className || field.class || null;
    delete sanitizedField.class;

    return utils.trimObj(sanitizedField);
  };

  // Begin the core plugin
  var rendered = [];

  // generate field markup if we have fields
  if (opts.formData) {
    for (var i = 0; i < opts.formData.length; i++) {
      let sanitizedField = santizeField(opts.formData[i]);
      rendered.push(_helpers.fieldRender(sanitizedField));
    }

    if (opts.render) {
      if (opts.container) {
        let renderedFormWrap = utils.markup('div', rendered, { className: 'rendered-form' });
        opts.container = (opts.container instanceof jQuery) ? opts.container[0] : opts.container;
        opts.container.emptyContainer();
        opts.container.appendChild(renderedFormWrap);
      } else if (element) {
        element.emptyContainer();
        element.appendFormFields(rendered);
      }

      runCallbacks();
      opts.notify.success(opts.label.formRendered);
    } else {
      formRender.markup = rendered.map(function(elem) {
        return elem.innerHTML;
      }).join('');
    }
  } else {
    let noData = utils.markup('div', opts.label.noFormData, {
      className: 'no-form-data'
    });
    rendered.push(noData);
    opts.notify.error(opts.label.noFormData);
  }

  return formRender;
}

(function($) {

  $.fn.formRender = function(options) {
    this.each(function() {
      let formRender = new FormRenderFn(options, this);
      return formRender;
    });
  };

})(jQuery);
<|endoftext|>


<commit_before>
import { HudObject } from './HudObject';

import {
	Vector3, Math as _Math, Face3, Color,
	Geometry, RingGeometry, CylinderBufferGeometry,
	MeshBasicMaterial, MeshPhongMaterial, MeshLambertMaterial,
	FrontSide, VertexColors, FlatShading,
	Mesh, Group
} from '../../../../three.js/src/Three';

function Compass ( container ) {

	var stdWidth  = HudObject.stdWidth;
	var stdMargin = HudObject.stdMargin;

	Group.call( this );

	this.name = 'CV.Compass';
	this.domObjects = [];

	var cg1 = new CylinderBufferGeometry( stdWidth * 0.90, stdWidth, 3, 32, 1, true );
	cg1.rotateX( Math.PI / 2 );

	var c1 = new Mesh( cg1, new MeshPhongMaterial( { color: 0x888888, specular: 0x888888 } ) );

	var cg2 = new RingGeometry( stdWidth * 0.9, stdWidth, 4, 1, -Math.PI / 32 + Math.PI / 2, Math.PI / 16 );
	cg2.translate( 0, 0, 5 );

	var c2 = new Mesh( cg2, new MeshBasicMaterial( { color: 0xb03a14 } ) );

	var r1 = _makeRose( stdWidth * 0.8, 0.141, 0x581d0a, 0x0c536a );
	var r2 = _makeRose( stdWidth * 0.9, 0.141, 0xb03a14, 0x1ab4e5 );

	r1.rotateZ( Math.PI / 4 );
	r1.merge( r2 );

	var rMesh = new Mesh( r1, new MeshLambertMaterial( { vertexColors: VertexColors, side: FrontSide, shading: FlatShading } ) );

	this.add( c1 );
	this.add( c2 );
	this.add( rMesh );

	var offset = stdWidth + stdMargin;

	this.translateX( -offset );
	this.translateY(  offset );

	this.lastRotation = 0;

	var panel = document.createElement( 'div' );

	panel.classList.add( 'cv-compass' );
	panel.textContent = '';

	container.appendChild( panel );

	this.txt = panel;
	this.domObjects.push( panel );

	this.addEventListener( 'removed', this.removeDomObjects );
	this.txt.textContent = '000\u00B0';

	return this;

	// make 'petal' for compass rose
	function _makePetal ( radius, scale, color1, color2 ) {

		var innerR = radius * scale;
		var g = new Geometry();

		g.vertices.push( new Vector3( 0, radius, 0 ) );
		g.vertices.push( new Vector3( innerR ,innerR, 0 ) );
		g.vertices.push( new Vector3( 0, 0, 14 * scale ) );
		g.vertices.push( new Vector3( -innerR, innerR, 0 ) );

		var f1 = new Face3( 0, 2, 1, new Vector3( 0, 0, 1 ), color1, 0 );
		var f2 = new Face3( 0, 3, 2, new Vector3( 0, 0, 1 ), color2, 0 );

		g.faces.push( f1 );
		g.faces.push( f2 );

		return g;

	}

	function _makeRose ( radius, scale, color1, color2 ) {

		var p1 = _makePetal( radius, scale, new Color( color1 ), new Color( color2 ) );
		var p2 = p1.clone();
		var p3 = p1.clone();
		var p4 = p1.clone();

		p2.rotateZ( Math.PI / 2 );
		p3.rotateZ( Math.PI );
		p4.rotateZ( Math.PI / 2 * 3 );

		p1.merge( p2 );
		p1.merge( p3 );
		p1.merge( p4 );

		p1.computeFaceNormals();

		return p1;

	}

}

Compass.prototype = Object.create( Group.prototype );

Object.assign( Compass.prototype, HudObject.prototype );

Compass.prototype.constructor = Compass;

Compass.prototype.set = function () {

	var direction     = new Vector3();
	var yAxis         = new Vector3( 0, 1, 0 );
	var negativeZAxis = new Vector3( 0, 0, -1 );

	return function set ( vCamera ) {

		vCamera.getWorldDirection( direction );

		if ( direction.x === 0 && direction.y === 0 ) {

			// FIXME get camera rotation....
			return;

		}

		// we are only interested in angle to horizontal plane.
		direction.z = 0;

		var a = direction.angleTo( yAxis );

		if ( direction.x >= 0 ) a = 2 * Math.PI - a;

		if ( a === this.lastRotation ) return;

		var degrees = 360 - Math.round( _Math.radToDeg( a ) );

		this.txt.textContent = degrees.toString().padStart( 3, '0' ) + '\u00B0'; // unicode degree symbol

		this.rotateOnAxis( negativeZAxis, a - this.lastRotation );

		this.lastRotation = a;

	};

} ();

export { Compass };

// EOF<commit_msg>migrate to three r57<commit_after>
import { HudObject } from './HudObject';

import {
	Vector3, Math as _Math, Face3, Color,
	Geometry, RingGeometry, CylinderBufferGeometry,
	MeshBasicMaterial, MeshPhongMaterial, MeshLambertMaterial,
	FrontSide, VertexColors,
	Mesh, Group
} from '../../../../three.js/src/Three';

function Compass ( container ) {

	var stdWidth  = HudObject.stdWidth;
	var stdMargin = HudObject.stdMargin;

	Group.call( this );

	this.name = 'CV.Compass';
	this.domObjects = [];

	var cg1 = new CylinderBufferGeometry( stdWidth * 0.90, stdWidth, 3, 32, 1, true );
	cg1.rotateX( Math.PI / 2 );

	var c1 = new Mesh( cg1, new MeshPhongMaterial( { color: 0x888888, specular: 0x888888 } ) );

	var cg2 = new RingGeometry( stdWidth * 0.9, stdWidth, 4, 1, -Math.PI / 32 + Math.PI / 2, Math.PI / 16 );
	cg2.translate( 0, 0, 5 );

	var c2 = new Mesh( cg2, new MeshBasicMaterial( { color: 0xb03a14 } ) );

	var r1 = _makeRose( stdWidth * 0.8, 0.141, 0x581d0a, 0x0c536a );
	var r2 = _makeRose( stdWidth * 0.9, 0.141, 0xb03a14, 0x1ab4e5 );

	r1.rotateZ( Math.PI / 4 );
	r1.merge( r2 );

	var rMesh = new Mesh( r1, new MeshLambertMaterial( { vertexColors: VertexColors, side: FrontSide, flatShading: true } ) );

	this.add( c1 );
	this.add( c2 );
	this.add( rMesh );

	var offset = stdWidth + stdMargin;

	this.translateX( -offset );
	this.translateY(  offset );

	this.lastRotation = 0;

	var panel = document.createElement( 'div' );

	panel.classList.add( 'cv-compass' );
	panel.textContent = '';

	container.appendChild( panel );

	this.txt = panel;
	this.domObjects.push( panel );

	this.addEventListener( 'removed', this.removeDomObjects );
	this.txt.textContent = '000\u00B0';

	return this;

	// make 'petal' for compass rose
	function _makePetal ( radius, scale, color1, color2 ) {

		var innerR = radius * scale;
		var g = new Geometry();

		g.vertices.push( new Vector3( 0, radius, 0 ) );
		g.vertices.push( new Vector3( innerR ,innerR, 0 ) );
		g.vertices.push( new Vector3( 0, 0, 14 * scale ) );
		g.vertices.push( new Vector3( -innerR, innerR, 0 ) );

		var f1 = new Face3( 0, 2, 1, new Vector3( 0, 0, 1 ), color1, 0 );
		var f2 = new Face3( 0, 3, 2, new Vector3( 0, 0, 1 ), color2, 0 );

		g.faces.push( f1 );
		g.faces.push( f2 );

		return g;

	}

	function _makeRose ( radius, scale, color1, color2 ) {

		var p1 = _makePetal( radius, scale, new Color( color1 ), new Color( color2 ) );
		var p2 = p1.clone();
		var p3 = p1.clone();
		var p4 = p1.clone();

		p2.rotateZ( Math.PI / 2 );
		p3.rotateZ( Math.PI );
		p4.rotateZ( Math.PI / 2 * 3 );

		p1.merge( p2 );
		p1.merge( p3 );
		p1.merge( p4 );

		p1.computeFaceNormals();

		return p1;

	}

}

Compass.prototype = Object.create( Group.prototype );

Object.assign( Compass.prototype, HudObject.prototype );

Compass.prototype.constructor = Compass;

Compass.prototype.set = function () {

	var direction     = new Vector3();
	var yAxis         = new Vector3( 0, 1, 0 );
	var negativeZAxis = new Vector3( 0, 0, -1 );

	return function set ( vCamera ) {

		vCamera.getWorldDirection( direction );

		if ( direction.x === 0 && direction.y === 0 ) {

			// FIXME get camera rotation....
			return;

		}

		// we are only interested in angle to horizontal plane.
		direction.z = 0;

		var a = direction.angleTo( yAxis );

		if ( direction.x >= 0 ) a = 2 * Math.PI - a;

		if ( a === this.lastRotation ) return;

		var degrees = 360 - Math.round( _Math.radToDeg( a ) );

		this.txt.textContent = degrees.toString().padStart( 3, '0' ) + '\u00B0'; // unicode degree symbol

		this.rotateOnAxis( negativeZAxis, a - this.lastRotation );

		this.lastRotation = a;

	};

} ();

export { Compass };

// EOF<|endoftext|>


<commit_before>import React from "react";
import WordlistStore from "../stores/WordlistStore";
import _ from "lodash";
import Notes from "../components/verb/Notes.js";
import Debouncer from "../components/Debouncer.js";

export default React.createClass({
  getInitialState: function() {
    return {
      words: undefined
    };
  },

  loadSample: function(sampleSize) {
    const wordToStateful = (w, caption) => {
      return {
        expected: w,
        actual: '',
        caption: caption,
      };
    };
    const words = _.sampleSize(WordlistStore.getAll(), sampleSize)
      .map((w)=>{
        const verb = _.pick(w, 'infinitive', 'notes');
        verb.imperfect_singular = wordToStateful(w.imperfect.singular, "Imperfectum singular");
        verb.imperfect_plural = wordToStateful(w.imperfect.plural, "Imperfectum plural");
        verb.perfect = wordToStateful(w.perfect, "Perfectum");
        return verb;
    });

    return words;
  },

  onGo: function(size) {
    this.setState({words: this.loadSample(size)})
  },

  componentWillReceiveProps: function(newProps) {
    this.setState(this.getInitialState());
  },

  render: function() {
    if (this.state.words) {
      return <RandomRow words={this.state.words} onDone={this.componentWillReceiveProps.bind(this)} />
    } else {
      return <SizeInput onGo={this.onGo}/>
    }
  }

});

const SizeInput = React.createClass({
  getInitialState: function() {return {value: 15};},
  setValue: function(newValue) {
    this.setState({value: parseInt(newValue)});
  },
  onGoClick: function() {
    if (this.state.value > 0) {
      this.props.onGo(this.state.value);
    }
  },
  render: function() {
    const valueLink = {
      value: this.state.value,
      requestChange: this.setValue
    };
    return (
      <form class="form-inline">

          Sample size
          <input type="number" class="form-control" id="sampleSize" autoFocus valueLink={valueLink}/>

        <button onClick={this.onGoClick} type="submit" class="btn btn-default">Randomize</button>
      </form>
    );
  }
});

const RandomRow = React.createClass({

  getInitialState: function() {
    return {
      position: 0,
      showAnswers : false,
    };
  },

  getCurrentWord: function() {
    return this.props.words[this.state.position];
  },

  onNextClick: function() {
    if (this.isEof()) {
      this.props.onDone();
    } else {
      this.setState({
        position: this.state.position + 1,
        showAnswers: false,
      });
    }
  },

  onHelpClickHandler: function() {
    this.setState({showAnswers: true});
  },

  isEof: function() {
    return this.state.position + 1 >= this.props.words.length;
  },

  render: function() {
    const verb = this.getCurrentWord();
    return  <VerbEntryForm key={verb.infinitive} verb={verb} showAnswers={this.state.showAnswers}>
              <button type="submit" class="btn btn-default" onClick={this.onNextClick}>{this.isEof() ? 'Opnieuw' : 'Next'}</button>
              <button type="submit" class="btn" onClick={this.onHelpClickHandler}>Help!</button>
            </VerbEntryForm>
  }
});

const VerbEntryForm = React.createClass({

  getInitialState: function() {
    return {
      verb: this.props.verb,
    };
  },

  render: function() {
    const {showAnswers} = this.props;
    const {verb} = this.state;
    const that = this;
    function onChangeHandlerFactory (key) {
      return function(e) {
        const newValue = e.target.value.toLowerCase();
        verb[key].actual = newValue;
        that.setState({verb});
      }
    }

    const inputFields = ['imperfect_singular', 'imperfect_plural', 'perfect'].map((key, index) => {
        const form = verb[key];
        return <FormGroup key={key} id={key} caption={form.caption} autoFocus={index == 0}
          text={form.expected} showAnswer={showAnswers} placeholder={verb.infinitive}
          value={form.actual} onChange={onChangeHandlerFactory(key)} />
    });

    return (
      <form>

{verb.infinitive}

<Notes text={verb.notes}/>

        {inputFields}
        {this.props.children}
      </form>
    );
  }
});

const FormGroup = React.createClass({
  mixins: [Debouncer(function(){this.reEvaluate()})],

  getInitialState: function() {return {evaluationClass: ''}},

  evaluateAnswer: function() {
     return (this.props.value == this.props.text) ? 'has-success' : 'has-error';
  },

  reEvaluate: function() {
    this.setValidationStatus(this.evaluateAnswer());
  },

  withdrawEvaluation: function() {
    this.setValidationStatus('');
  },

  setValidationStatus: function(evaluationClass) {
    this.setState({evaluationClass});
  },

  componentWillReceiveProps: function(newProps) {
    if (this.props.text != newProps.text || this.props.value != newProps.value) {
      this.withdrawEvaluation();
      this.debounce();
    }
  },

  _onKeyDown : function(event) {
    const ENTER_KEY_CODE = 13;
    /*if (event.keyCode === ENTER_KEY_CODE) {
      event.preventDefault();
      return false;
    }*/
  },

  render: function() {
    const {id, caption, showAnswer, ...rest} = this.props;

    return

            {caption}{showAnswer ? ' -> ' + this.props.text : ''}
            <input {...rest}
              id={id}
              className="form-control input-lg"
              type="text"
              autoComplete="off"
              autoCapitalize="none"
              autoCorrect="off"
              onKeyDown={this._onKeyDown}
              onBlur={this.reEvaluate}
              />

  }
});<commit_msg>do not let next on enter with not yet valid form<commit_after>import React from "react";
import WordlistStore from "../stores/WordlistStore";
import _ from "lodash";
import Notes from "../components/verb/Notes.js";
import Debouncer from "../components/Debouncer.js";

export default React.createClass({
  getInitialState: function() {
    return {
      words: undefined
    };
  },

  loadSample: function(sampleSize) {
    const wordToStateful = (w, caption) => {
      return {
        expected: w,
        actual: '',
        caption: caption,
      };
    };
    const words = _.sampleSize(WordlistStore.getAll(), sampleSize)
      .map((w)=>{
        const verb = _.pick(w, 'infinitive', 'notes');
        verb.imperfect_singular = wordToStateful(w.imperfect.singular, "Imperfectum singular");
        verb.imperfect_plural = wordToStateful(w.imperfect.plural, "Imperfectum plural");
        verb.perfect = wordToStateful(w.perfect, "Perfectum");
        return verb;
    });

    return words;
  },

  onGo: function(size) {
    this.setState({words: this.loadSample(size)})
  },

  componentWillReceiveProps: function(newProps) {
    this.setState(this.getInitialState());
  },

  render: function() {
    if (this.state.words) {
      return <RandomRow words={this.state.words} onDone={this.componentWillReceiveProps.bind(this)} />
    } else {
      return <SizeInput onGo={this.onGo}/>
    }
  }

});

const SizeInput = React.createClass({
  getInitialState: function() {return {value: 15};},
  setValue: function(newValue) {
    this.setState({value: parseInt(newValue)});
  },
  onGoClick: function() {
    if (this.state.value > 0) {
      this.props.onGo(this.state.value);
    }
  },
  render: function() {
    const valueLink = {
      value: this.state.value,
      requestChange: this.setValue
    };
    return (
      <form class="form-inline">

          Sample size
          <input type="number" class="form-control" id="sampleSize" autoFocus valueLink={valueLink}/>

        <button onClick={this.onGoClick} type="submit" class="btn btn-default">Randomize</button>
      </form>
    );
  }
});

const RandomRow = React.createClass({

  getInitialState: function() {
    return {
      position: 0,
      showAnswers : false,
    };
  },

  getCurrentWord: function() {
    return this.props.words[this.state.position];
  },

  onNextClick: function() {
    if (this.isEof()) {
      this.props.onDone();
    } else {
      this.setState({
        position: this.state.position + 1,
        showAnswers: false,
      });
    }
  },

  onHelpClickHandler: function() {
    this.setState({showAnswers: true});
  },

  isEof: function() {
    return this.state.position + 1 >= this.props.words.length;
  },

  render: function() {
    const verb = this.getCurrentWord();
    return  <VerbEntryForm key={verb.infinitive} verb={verb} showAnswers={this.state.showAnswers}>
              <button type="submit" class="btn btn-default" onClick={this.onNextClick}>{this.isEof() ? 'Opnieuw' : 'Next'}</button>
              <button type="submit" class="btn" onClick={this.onHelpClickHandler}>Help!</button>
            </VerbEntryForm>
  }
});

const VerbEntryForm = React.createClass({

  getInitialState: function() {
    return {
      verb: this.props.verb,
    };
  },

  render: function() {
    const {showAnswers} = this.props;
    const {verb} = this.state;
    const that = this;
    function onChangeHandlerFactory (key) {
      return function(e) {
        const newValue = e.target.value.toLowerCase();
        verb[key].actual = newValue;
        that.setState({verb});
      }
    }

    const inputFields = ['imperfect_singular', 'imperfect_plural', 'perfect'].map((key, index) => {
        const form = verb[key];
        return <FormGroup key={key} id={key} caption={form.caption} autoFocus={index == 0}
          text={form.expected} showAnswer={showAnswers} placeholder={verb.infinitive}
          value={form.actual} onChange={onChangeHandlerFactory(key)} />
    });

    return (
      <form onKeyDown={this._onKeyDown}>

{verb.infinitive}

<Notes text={verb.notes}/>

        {inputFields}
        {this.props.children}
      </form>
    );
  },

  _allAnsweredCorrectly: function() {
    const {verb} = this.state;
    //TODO: describe at the model
    const anyFalse = _.find(['imperfect_singular', 'imperfect_plural', 'perfect'], (key) => {
      return verb[key].actual != verb[key].expected;
    });
    return !anyFalse;
  },

  _onKeyDown : function(event) {
    const ENTER_KEY_CODE = 13;
    if (event.keyCode == ENTER_KEY_CODE && !this._allAnsweredCorrectly()) {
      event.preventDefault();
      return false;
    }
    return false;
  },

});

const FormGroup = React.createClass({
  mixins: [Debouncer(function(){this.reEvaluate()})],

  getInitialState: function() {return {evaluationClass: ''}},

  evaluateAnswer: function() {
     return (this.props.value == this.props.text) ? 'has-success' : 'has-error';
  },

  reEvaluate: function() {
    this.setValidationStatus(this.evaluateAnswer());
  },

  withdrawEvaluation: function() {
    this.setValidationStatus('');
  },

  setValidationStatus: function(evaluationClass) {
    this.setState({evaluationClass});
  },

  componentWillReceiveProps: function(newProps) {
    if (this.props.text != newProps.text || this.props.value != newProps.value) {
      this.withdrawEvaluation();
      this.debounce();
    }
  },

  render: function() {
    const {id, caption, showAnswer, ...rest} = this.props;

    return

            {caption}{showAnswer ? ' -> ' + this.props.text : ''}
            <input {...rest}
              id={id}
              className="form-control input-lg"
              type="text"
              autoComplete="off"
              autoCapitalize="none"
              autoCorrect="off"
              onBlur={this.reEvaluate}
              />

  }
});<|endoftext|>


<commit_before>Math.randomBelow = function (n) {
    'use strict';
    if (n < 1) {
        throw "invalid argument: " + n;
    }
    return Math.floor(Math.random() * n);
};<commit_msg>fixing the build<commit_after>Math.randomBelow = function (n) {
    'use strict';
    if (n < 1) {
        throw 'invalid argument: ' + n;
    }
    return Math.floor(Math.random() * n);
};<|endoftext|>


<commit_before>import { LineSegmentsGeometry } from '../core/LineSegmentsGeometry';
import { LineSegments2 } from '../core/LineSegments2';

function Grid ( ctx ) {

	const geometry = new LineSegmentsGeometry();
	const survey = ctx.survey;

	LineSegments2.call( this, geometry, ctx.materials.getLine2Material( 'basivc' ) );

	this.material.color.set( 0xffffff );
	this.scale.set( 1, 1, 1 );
	this.type = 'CV.Grid';

	const box = ctx.survey.combinedLimits;

	const a = box.min.clone(); a.y = box.max.y;
	const b = box.max.clone();
	const c = box.min.clone(); c.x = box.max.x;
	const d = box.min.clone();

	/*
		Convert to original model CRS

		A-B
		| |
		D-C

	*/

	const A = survey.getGeographicalPosition( a );
	const B = survey.getGeographicalPosition( b );
	const C = survey.getGeographicalPosition( c );
	const D = survey.getGeographicalPosition( d );

	// approximate tranform as rotation and scale for small areas.

	const xRange = Math.min( C.x - D.x, B.x - A.x );
	const yRange = Math.min( A.y - D.y, B.y - C.y );

	const r = Math.log10( Math.max( xRange, yRange ) );
	const interval = Math.pow( 10, Math.round( r ) - 1 );

	const xScale = ( C.x - D.x ) / ( c.x - d.x );
	const yScale = ( A.y - D.y ) / ( a.y - d.y );

	const theta = Math.atan2( C.y - D.y, C.x - D.x );
	const cos = Math.cos( theta );

	const xOffset = ( interval - ( D.x % interval ) ) * cos / xScale;
	const yOffset = ( interval - ( D.y % interval ) ) * cos / yScale;

	const deltaX = interval * cos / xScale;
	const deltaY = interval * cos / yScale;

	// assume linear relationship between grids for simplicity
	const hGrad = ( c.x - d.x) * ( C.y - D.y ) / ( C.x - D.x );
	const vGrad = ( a.y - d.y) * ( A.x - D.x ) / ( A.y - D.y );

	const z = box.min.z;
	let i;

	const baseColor = ctx.cfg.themeColor( 'grid.base' );

	const colors = [];
	const vertices = [];

	for ( i = d.x + xOffset; i < c.x; i += deltaX ) {

		vertices.push( i, d.y, z, i - vGrad, a.y, z );
		colors.push( baseColor.r, baseColor.g, baseColor.b, baseColor.r, baseColor.g, baseColor.b );

	}

	for ( i = d.y + yOffset; i < a.y; i += deltaY ) {

		vertices.push( d.x,  i, z, c.x , i - hGrad, z );
		colors.push( baseColor.r, baseColor.g, baseColor.b, baseColor.r, baseColor.g, baseColor.b );

	}

	geometry.setPositions( vertices );
	geometry.setColors( colors );

}

Grid.prototype = Object.create( LineSegments2.prototype );

export { Grid };<commit_msg>remove redundant setting<commit_after>import { LineSegmentsGeometry } from '../core/LineSegmentsGeometry';
import { LineSegments2 } from '../core/LineSegments2';

function Grid ( ctx ) {

	const geometry = new LineSegmentsGeometry();
	const survey = ctx.survey;

	LineSegments2.call( this, geometry, ctx.materials.getLine2Material( 'basivc' ) );

	this.scale.set( 1, 1, 1 );
	this.type = 'CV.Grid';

	const box = ctx.survey.combinedLimits;

	const a = box.min.clone(); a.y = box.max.y;
	const b = box.max.clone();
	const c = box.min.clone(); c.x = box.max.x;
	const d = box.min.clone();

	/*
		Convert to original model CRS

		A-B
		| |
		D-C

	*/

	const A = survey.getGeographicalPosition( a );
	const B = survey.getGeographicalPosition( b );
	const C = survey.getGeographicalPosition( c );
	const D = survey.getGeographicalPosition( d );

	// approximate tranform as rotation and scale for small areas.

	const xRange = Math.min( C.x - D.x, B.x - A.x );
	const yRange = Math.min( A.y - D.y, B.y - C.y );

	const r = Math.log10( Math.max( xRange, yRange ) );
	const interval = Math.pow( 10, Math.round( r ) - 1 );

	const xScale = ( C.x - D.x ) / ( c.x - d.x );
	const yScale = ( A.y - D.y ) / ( a.y - d.y );

	const theta = Math.atan2( C.y - D.y, C.x - D.x );
	const cos = Math.cos( theta );

	const xOffset = ( interval - ( D.x % interval ) ) * cos / xScale;
	const yOffset = ( interval - ( D.y % interval ) ) * cos / yScale;

	const deltaX = interval * cos / xScale;
	const deltaY = interval * cos / yScale;

	// assume linear relationship between grids for simplicity
	const hGrad = ( c.x - d.x) * ( C.y - D.y ) / ( C.x - D.x );
	const vGrad = ( a.y - d.y) * ( A.x - D.x ) / ( A.y - D.y );

	const z = box.min.z;
	let i;

	const baseColor = ctx.cfg.themeColor( 'grid.base' );

	const colors = [];
	const vertices = [];

	for ( i = d.x + xOffset; i < c.x; i += deltaX ) {

		vertices.push( i, d.y, z, i - vGrad, a.y, z );
		colors.push( baseColor.r, baseColor.g, baseColor.b, baseColor.r, baseColor.g, baseColor.b );

	}

	for ( i = d.y + yOffset; i < a.y; i += deltaY ) {

		vertices.push( d.x,  i, z, c.x , i - hGrad, z );
		colors.push( baseColor.r, baseColor.g, baseColor.b, baseColor.r, baseColor.g, baseColor.b );

	}

	geometry.setPositions( vertices );
	geometry.setColors( colors );

}

Grid.prototype = Object.create( LineSegments2.prototype );

export { Grid };<|endoftext|>