<commit_before>/*
 * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of iotagent-ul
 *
 * iotagent-ul is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * iotagent-ul is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with iotagent-ul.
 * If not, seehttp://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[iot_support@tid.es]
 *
 * Modified by: Fernando Méndez, Daniel Calvo - ATOS Research & Innovation
 */

'use strict';

var iotAgentLib = require('iotagent-node-lib'),
    config = require('./configService'),
    transportSelector = require('./transportSelector'),
    intoTrans = iotAgentLib.intoTrans,
    _ = require('underscore'),
    utils = require('./iotaUtils'),
    async = require('async'),
    ulParser = require('./ulParser'),
    constants = require('./constants'),
    context = {
        op: 'IOTAUL.Common.Binding'
    };

/**
 * Find the attribute given by its name between all the active attributes of the given device, returning its type, or
 * null otherwise.
 *
 * @param {String} attribute        Name of the attribute to find.
 * @param {Object} device           Device object containing all the information about a device.
 * @return {String}                 String identifier of the attribute type.
 */
function guessType(attribute, device) {
    for (var i = 0; i < device.active.length; i++) {
        if (device.active[i].name === attribute) {
            return device.active[i].type;
        }
    }

    if (attribute === constants.TIMESTAMP_ATTRIBUTE) {
        if (iotAgentLib.configModule.checkNgsi2()) {
            return constants.TIMESTAMP_TYPE_NGSI2;
        } else {
            return constants.TIMESTAMP_TYPE;
        }
    } else {
        return constants.DEFAULT_ATTRIBUTE_TYPE;
    }
}

function sendConfigurationToDevice(device, apiKey, deviceId, results, callback) {
    transportSelector.applyFunctionFromBinding(
        [apiKey, deviceId, results],
        'sendConfigurationToDevice',
        device.transport,
        callback
    );
}

/**
 * Deals with configuration requests coming from the device. Whenever a new configuration requests arrives with a list
 * of attributes to retrieve, this handler asks the Context Broker for the values of those attributes, and publish a
 * new message in the "/1234/MQTT_2/configuration/values" topic
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} deviceId         Id of the device to be updated.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Object} objMessage          JSON object received.
 */
function manageConfigurationRequest(apiKey, deviceId, device, objMessage) {
    utils.manageConfiguration(
        apiKey,
        deviceId,
        device,
        objMessage,
        async.apply(sendConfigurationToDevice, device),
        function(error) {
            if (error) {
                iotAgentLib.alarms.raise(constants.MQTTB_ALARM, error);
            } else {
                iotAgentLib.alarms.release(constants.MQTTB_ALARM);
                config
                    .getLogger()
                    .debug(context, 'Configuration request finished for APIKey [%s] and Device [%s]', apiKey, deviceId);
            }
        }
    );
}

/**
 * Utility function used to reduce UL Payload group arrays and process them to update their value in the Context Broker.
 * This function process a new element of the payload group, generating a function ready to be called with a callback
 * in a async.series() call.
 *
 * @param {Object} device       Object containing all the information about the device sending the measures.
 * @param {String} apikey       APIKey of the service the device belongs to.
 * @param {Array} previous      Array of prepared functions that send information to the Context Broker.
 * @param {Object} current      Information sent by the device.
 * @param {Number} index        Index of the group in the array.
 * @return {Array}             Updated array of functions.
 */
function processMeasureGroup(device, apikey, previous, current, index) {
    var values = [];

    if (current.command) {
        previous.push(
            iotAgentLib.setCommandResult.bind(
                null,
                device.name,
                config.getConfig().iota.defaultResource,
                apikey,
                current.command,
                current.value,
                constants.COMMAND_STATUS_COMPLETED,
                device
            )
        );
    } else {
        for (var i in current) {
            if (current.hasOwnProperty(i)) {
                values.push({
                    name: i,
                    type: guessType(i, device),
                    value: current[i]
                });
            }
        }

        previous.push(iotAgentLib.update.bind(null, device.name, device.type, '', values, device));
    }

    return previous;
}

/**
 * Adds multiple measures to the Context Broker. Multiple measures come in the form of single-level JSON objects,
 * whose keys are the attribute names and whose values are the attribute values.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {String} message          UL payload.
 */
function multipleMeasures(apiKey, device, message) {
    var updates = [],
        messageStr = message.toString(),
        parsedMessage;

    config.getLogger().debug('Processing multiple measures for device [%s] with apiKey [%s]', device.id, apiKey);

    try {
        parsedMessage = ulParser.parse(messageStr);
    } catch (e) {
        config.getLogger().error(context, 'MEASURES-003: Parse error parsing incoming message [%]', messageStr);
        return;
    }

    updates = parsedMessage.reduce(processMeasureGroup.bind(null, device, apiKey), []);

    async.series(updates, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Multiple measures for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Adds a single measure to the context broker. The message for single measures contains the direct value to
 * be inserted in the attribute, given by its name.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} attribute        Name of the attribute to update.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Buffer} message          Raw message coming from the client.
 */
function singleMeasure(apiKey, attribute, device, message) {
    var values;

    config.getLogger().debug('Processing single measure for device [%s] with apiKey [%s]', device.id, apiKey);

    values = [
        {
            name: attribute,
            type: guessType(attribute, device),
            value: message.toString()
        }
    ];

    iotAgentLib.update(device.name, device.type, '', values, device, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Single measure for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Handles an incoming message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      message body (Object or Buffer, depending on the value).
 */
function messageHandler(topic, message, protocol) {
    var topicInformation = topic.split('/'),
        topicProtocol,
        apiKey,
        deviceId,
        messageStr,
        parsedMessage;

    if (['json', 'ul'].includes(topicInformation[1].toLowerCase())) {
        topicProtocol = topicInformation[1].toLowerCase();
        apiKey = topicInformation[2];
        deviceId = topicInformation[3];
    } else {
        topicProtocol = 'ul';
        apiKey = topicInformation[1];
        deviceId = topicInformation[2];
    }
    if (topicProtocol !== 'ul') {
        config.getLogger().debug(context, 'Topic protocol [%s] not expected by this agent ', topicProtocol);
        return;
    }
    messageStr = message.toString();

    function processMessageForDevice(device, apiKey, topicInformation) {
        iotAgentLib.alarms.release(constants.MQTTB_ALARM);

        if (
            topicInformation[3] === constants.CONFIGURATION_SUFIX &&
            topicInformation[4] === constants.CONFIGURATION_COMMAND_SUFIX &&
            message
        ) {
            parsedMessage = ulParser.parseConfigurationRequest(messageStr);
            manageConfigurationRequest(apiKey, deviceId, device, parsedMessage);
        } else if (topicInformation[3] === constants.CONFIGURATION_COMMAND_UPDATE) {
            var commandObj = ulParser.result(message.toString());
            utils.updateCommand(
                apiKey,
                device,
                commandObj.result,
                commandObj.command,
                constants.COMMAND_STATUS_COMPLETED,
                function(error) {
                    config.getLogger().debug('Command updated with result: %s', error);
                }
            );
        } else if (topicInformation[4]) {
            singleMeasure(apiKey, topicInformation[4], device, message);
        } else if (topicInformation[3] === constants.MEASURES_SUFIX) {
            multipleMeasures(apiKey, device, message.toString());
        } else {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                "MEASURES-004: Couldn't process message [%s] due to format issues.",
                /*jshint quotmark: single */
                message
            );
        }
    }

    function processDeviceMeasure(error, device) {
        if (error) {
            config.getLogger().error(context, 'MEASURES-005: Error before processing device measures [%s]', topic);
        } else {
            var localContext = _.clone(context);

            localContext.service = device.service;
            localContext.subservice = device.subservice;

            intoTrans(localContext, processMessageForDevice)(device, apiKey, topicInformation);
        }
    }

    utils.retrieveDevice(deviceId, apiKey, protocol, processDeviceMeasure);
}

/**
 * Handles an incoming AMQP message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the AMQP topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      AMQP message body (Object or Buffer, depending on the value).
 */
function amqpMessageHandler(topic, message) {
    messageHandler(topic, message, 'AMQP');
}

/**
 * Handles an incoming MQTT message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the MQTT topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      MQTT message body (Object or Buffer, depending on the value).
 */
function mqttMessageHandler(topic, message) {
    config.getLogger().debug(context, 'message topic: %s', topic);
    messageHandler(topic, message, 'MQTT');
}

exports.amqpMessageHandler = amqpMessageHandler;
exports.mqttMessageHandler = mqttMessageHandler;
exports.messageHandler = messageHandler;
exports.processMeasureGroup = processMeasureGroup;
exports.guessType = guessType;
<commit_msg>simpligy protocol in topic handling<commit_after>/*
 * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of iotagent-ul
 *
 * iotagent-ul is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * iotagent-ul is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with iotagent-ul.
 * If not, seehttp://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[iot_support@tid.es]
 *
 * Modified by: Fernando Méndez, Daniel Calvo - ATOS Research & Innovation
 */

'use strict';

var iotAgentLib = require('iotagent-node-lib'),
    config = require('./configService'),
    transportSelector = require('./transportSelector'),
    intoTrans = iotAgentLib.intoTrans,
    _ = require('underscore'),
    utils = require('./iotaUtils'),
    async = require('async'),
    ulParser = require('./ulParser'),
    constants = require('./constants'),
    context = {
        op: 'IOTAUL.Common.Binding'
    };

/**
 * Find the attribute given by its name between all the active attributes of the given device, returning its type, or
 * null otherwise.
 *
 * @param {String} attribute        Name of the attribute to find.
 * @param {Object} device           Device object containing all the information about a device.
 * @return {String}                 String identifier of the attribute type.
 */
function guessType(attribute, device) {
    for (var i = 0; i < device.active.length; i++) {
        if (device.active[i].name === attribute) {
            return device.active[i].type;
        }
    }

    if (attribute === constants.TIMESTAMP_ATTRIBUTE) {
        if (iotAgentLib.configModule.checkNgsi2()) {
            return constants.TIMESTAMP_TYPE_NGSI2;
        } else {
            return constants.TIMESTAMP_TYPE;
        }
    } else {
        return constants.DEFAULT_ATTRIBUTE_TYPE;
    }
}

function sendConfigurationToDevice(device, apiKey, deviceId, results, callback) {
    transportSelector.applyFunctionFromBinding(
        [apiKey, deviceId, results],
        'sendConfigurationToDevice',
        device.transport,
        callback
    );
}

/**
 * Deals with configuration requests coming from the device. Whenever a new configuration requests arrives with a list
 * of attributes to retrieve, this handler asks the Context Broker for the values of those attributes, and publish a
 * new message in the "/1234/MQTT_2/configuration/values" topic
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} deviceId         Id of the device to be updated.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Object} objMessage          JSON object received.
 */
function manageConfigurationRequest(apiKey, deviceId, device, objMessage) {
    utils.manageConfiguration(
        apiKey,
        deviceId,
        device,
        objMessage,
        async.apply(sendConfigurationToDevice, device),
        function(error) {
            if (error) {
                iotAgentLib.alarms.raise(constants.MQTTB_ALARM, error);
            } else {
                iotAgentLib.alarms.release(constants.MQTTB_ALARM);
                config
                    .getLogger()
                    .debug(context, 'Configuration request finished for APIKey [%s] and Device [%s]', apiKey, deviceId);
            }
        }
    );
}

/**
 * Utility function used to reduce UL Payload group arrays and process them to update their value in the Context Broker.
 * This function process a new element of the payload group, generating a function ready to be called with a callback
 * in a async.series() call.
 *
 * @param {Object} device       Object containing all the information about the device sending the measures.
 * @param {String} apikey       APIKey of the service the device belongs to.
 * @param {Array} previous      Array of prepared functions that send information to the Context Broker.
 * @param {Object} current      Information sent by the device.
 * @param {Number} index        Index of the group in the array.
 * @return {Array}             Updated array of functions.
 */
function processMeasureGroup(device, apikey, previous, current, index) {
    var values = [];

    if (current.command) {
        previous.push(
            iotAgentLib.setCommandResult.bind(
                null,
                device.name,
                config.getConfig().iota.defaultResource,
                apikey,
                current.command,
                current.value,
                constants.COMMAND_STATUS_COMPLETED,
                device
            )
        );
    } else {
        for (var i in current) {
            if (current.hasOwnProperty(i)) {
                values.push({
                    name: i,
                    type: guessType(i, device),
                    value: current[i]
                });
            }
        }

        previous.push(iotAgentLib.update.bind(null, device.name, device.type, '', values, device));
    }

    return previous;
}

/**
 * Adds multiple measures to the Context Broker. Multiple measures come in the form of single-level JSON objects,
 * whose keys are the attribute names and whose values are the attribute values.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {String} message          UL payload.
 */
function multipleMeasures(apiKey, device, message) {
    var updates = [],
        messageStr = message.toString(),
        parsedMessage;

    config.getLogger().debug('Processing multiple measures for device [%s] with apiKey [%s]', device.id, apiKey);

    try {
        parsedMessage = ulParser.parse(messageStr);
    } catch (e) {
        config.getLogger().error(context, 'MEASURES-003: Parse error parsing incoming message [%]', messageStr);
        return;
    }

    updates = parsedMessage.reduce(processMeasureGroup.bind(null, device, apiKey), []);

    async.series(updates, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Multiple measures for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Adds a single measure to the context broker. The message for single measures contains the direct value to
 * be inserted in the attribute, given by its name.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} attribute        Name of the attribute to update.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Buffer} message          Raw message coming from the client.
 */
function singleMeasure(apiKey, attribute, device, message) {
    var values;

    config.getLogger().debug('Processing single measure for device [%s] with apiKey [%s]', device.id, apiKey);

    values = [
        {
            name: attribute,
            type: guessType(attribute, device),
            value: message.toString()
        }
    ];

    iotAgentLib.update(device.name, device.type, '', values, device, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Single measure for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Handles an incoming message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      message body (Object or Buffer, depending on the value).
 */
function messageHandler(topic, message, protocol) {
    var topicInformation = topic.split('/'),
        topicProtocol,
        apiKey,
        deviceId,
        messageStr,
        parsedMessage;

    if ('ul' === topicInformation[1].toLowerCase()) {
        apiKey = topicInformation[2];
        deviceId = topicInformation[3];
    } else {
        apiKey = topicInformation[1];
        deviceId = topicInformation[2];
    }
    messageStr = message.toString();

    function processMessageForDevice(device, apiKey, topicInformation) {
        iotAgentLib.alarms.release(constants.MQTTB_ALARM);

        if (
            topicInformation[3] === constants.CONFIGURATION_SUFIX &&
            topicInformation[4] === constants.CONFIGURATION_COMMAND_SUFIX &&
            message
        ) {
            parsedMessage = ulParser.parseConfigurationRequest(messageStr);
            manageConfigurationRequest(apiKey, deviceId, device, parsedMessage);
        } else if (topicInformation[3] === constants.CONFIGURATION_COMMAND_UPDATE) {
            var commandObj = ulParser.result(message.toString());
            utils.updateCommand(
                apiKey,
                device,
                commandObj.result,
                commandObj.command,
                constants.COMMAND_STATUS_COMPLETED,
                function(error) {
                    config.getLogger().debug('Command updated with result: %s', error);
                }
            );
        } else if (topicInformation[4]) {
            singleMeasure(apiKey, topicInformation[4], device, message);
        } else if (topicInformation[3] === constants.MEASURES_SUFIX) {
            multipleMeasures(apiKey, device, message.toString());
        } else {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                "MEASURES-004: Couldn't process message [%s] due to format issues.",
                /*jshint quotmark: single */
                message
            );
        }
    }

    function processDeviceMeasure(error, device) {
        if (error) {
            config.getLogger().error(context, 'MEASURES-005: Error before processing device measures [%s]', topic);
        } else {
            var localContext = _.clone(context);

            localContext.service = device.service;
            localContext.subservice = device.subservice;

            intoTrans(localContext, processMessageForDevice)(device, apiKey, topicInformation);
        }
    }

    utils.retrieveDevice(deviceId, apiKey, protocol, processDeviceMeasure);
}

/**
 * Handles an incoming AMQP message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the AMQP topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      AMQP message body (Object or Buffer, depending on the value).
 */
function amqpMessageHandler(topic, message) {
    messageHandler(topic, message, 'AMQP');
}

/**
 * Handles an incoming MQTT message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the MQTT topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      MQTT message body (Object or Buffer, depending on the value).
 */
function mqttMessageHandler(topic, message) {
    config.getLogger().debug(context, 'message topic: %s', topic);
    messageHandler(topic, message, 'MQTT');
}

exports.amqpMessageHandler = amqpMessageHandler;
exports.mqttMessageHandler = mqttMessageHandler;
exports.messageHandler = messageHandler;
exports.processMeasureGroup = processMeasureGroup;
exports.guessType = guessType;
<|endoftext|>


<commit_before>/*
 * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of iotagent-ul
 *
 * iotagent-ul is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * iotagent-ul is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with iotagent-ul.
 * If not, seehttp://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[iot_support@tid.es]
 *
 * Modified by: Fernando Méndez, Daniel Calvo - ATOS Research & Innovation
 */

'use strict';

var iotAgentLib = require('iotagent-node-lib'),
    config = require('./configService'),
    transportSelector = require('./transportSelector'),
    intoTrans = iotAgentLib.intoTrans,
    _ = require('underscore'),
    utils = require('./iotaUtils'),
    async = require('async'),
    ulParser = require('./ulParser'),
    constants = require('./constants'),
    context = {
        op: 'IOTAUL.Common.Binding'
    };

/**
 * Find the attribute given by its name between all the active attributes of the given device, returning its type, or
 * null otherwise.
 *
 * @param {String} attribute        Name of the attribute to find.
 * @param {Object} device           Device object containing all the information about a device.
 * @return {String}                 String identifier of the attribute type.
 */
function guessType(attribute, device) {
    for (var i = 0; i < device.active.length; i++) {
        if (device.active[i].name === attribute) {
            return device.active[i].type;
        }
    }

    if (attribute === constants.TIMESTAMP_ATTRIBUTE) {
        if (iotAgentLib.configModule.checkNgsi2()) {
            return constants.TIMESTAMP_TYPE_NGSI2;
        } else {
            return constants.TIMESTAMP_TYPE;
        }
    } else {
        return constants.DEFAULT_ATTRIBUTE_TYPE;
    }
}

function sendConfigurationToDevice(device, apiKey, deviceId, results, callback) {
    transportSelector.applyFunctionFromBinding(
        [apiKey, deviceId, results],
        'sendConfigurationToDevice',
        device.transport,
        callback
    );
}

/**
 * Deals with configuration requests coming from the device. Whenever a new configuration requests arrives with a list
 * of attributes to retrieve, this handler asks the Context Broker for the values of those attributes, and publish a
 * new message in the "/1234/MQTT_2/configuration/values" topic
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} deviceId         Id of the device to be updated.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Object} objMessage          JSON object received.
 */
function manageConfigurationRequest(apiKey, deviceId, device, objMessage) {
    utils.manageConfiguration(
        apiKey,
        deviceId,
        device,
        objMessage,
        async.apply(sendConfigurationToDevice, device),
        function(error) {
            if (error) {
                iotAgentLib.alarms.raise(constants.MQTTB_ALARM, error);
            } else {
                iotAgentLib.alarms.release(constants.MQTTB_ALARM);
                config
                    .getLogger()
                    .debug(context, 'Configuration request finished for APIKey [%s] and Device [%s]', apiKey, deviceId);
            }
        }
    );
}

/**
 * Utility function used to reduce UL Payload group arrays and process them to update their value in the Context Broker.
 * This function process a new element of the payload group, generating a function ready to be called with a callback
 * in a async.series() call.
 *
 * @param {Object} device       Object containing all the information about the device sending the measures.
 * @param {String} apikey       APIKey of the service the device belongs to.
 * @param {Array} previous      Array of prepared functions that send information to the Context Broker.
 * @param {Object} current      Information sent by the device.
 * @param {Number} index        Index of the group in the array.
 * @return {Array}             Updated array of functions.
 */
function processMeasureGroup(device, apikey, previous, current, index) {
    var values = [];

    if (current.command) {
        previous.push(
            iotAgentLib.setCommandResult.bind(
                null,
                device.name,
                config.getConfig().iota.defaultResource,
                apikey,
                current.command,
                current.value,
                constants.COMMAND_STATUS_COMPLETED,
                device
            )
        );
    } else {
        for (var i in current) {
            if (current.hasOwnProperty(i)) {
                values.push({
                    name: i,
                    type: guessType(i, device),
                    value: current[i]
                });
            }
        }

        previous.push(iotAgentLib.update.bind(null, device.name, device.type, '', values, device));
    }

    return previous;
}

/**
 * Adds multiple measures to the Context Broker. Multiple measures come in the form of single-level JSON objects,
 * whose keys are the attribute names and whose values are the attribute values.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {String} message          UL payload.
 */
function multipleMeasures(apiKey, device, message) {
    var updates = [],
        messageStr = message.toString(),
        parsedMessage;

    config.getLogger().debug('Processing multiple measures for device [%s] with apiKey [%s]', device.id, apiKey);

    try {
        parsedMessage = ulParser.parse(messageStr);
    } catch (e) {
        config.getLogger().error(context, 'MEASURES-003: Parse error parsing incoming message [%]', messageStr);
        return;
    }

    updates = parsedMessage.reduce(processMeasureGroup.bind(null, device, apiKey), []);

    async.series(updates, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Multiple measures for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Adds a single measure to the context broker. The message for single measures contains the direct value to
 * be inserted in the attribute, given by its name.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} attribute        Name of the attribute to update.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Buffer} message          Raw message coming from the client.
 */
function singleMeasure(apiKey, attribute, device, message) {
    var values;

    config.getLogger().debug('Processing single measure for device [%s] with apiKey [%s]', device.id, apiKey);

    values = [
        {
            name: attribute,
            type: guessType(attribute, device),
            value: message.toString()
        }
    ];

    iotAgentLib.update(device.name, device.type, '', values, device, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Single measure for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Handles an incoming message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      message body (Object or Buffer, depending on the value).
 */
function messageHandler(topic, message, protocol) {
    var topicInformation = topic.split('/'),
        apiKey,
        deviceId,
        messageStr,
        parsedMessage;

    if ('ul' === topicInformation[1].toLowerCase()) {
        apiKey = topicInformation[2];
        deviceId = topicInformation[3];
    } else {
        apiKey = topicInformation[1];
        deviceId = topicInformation[2];
    }
    messageStr = message.toString();

    function processMessageForDevice(device, apiKey, topicInformation) {
        iotAgentLib.alarms.release(constants.MQTTB_ALARM);

        if (
            topicInformation[3] === constants.CONFIGURATION_SUFIX &&
            topicInformation[4] === constants.CONFIGURATION_COMMAND_SUFIX &&
            message
        ) {
            parsedMessage = ulParser.parseConfigurationRequest(messageStr);
            manageConfigurationRequest(apiKey, deviceId, device, parsedMessage);
        } else if (topicInformation[3] === constants.CONFIGURATION_COMMAND_UPDATE) {
            var commandObj = ulParser.result(message.toString());
            utils.updateCommand(
                apiKey,
                device,
                commandObj.result,
                commandObj.command,
                constants.COMMAND_STATUS_COMPLETED,
                function(error) {
                    config.getLogger().debug('Command updated with result: %s', error);
                }
            );
        } else if (topicInformation[4]) {
            singleMeasure(apiKey, topicInformation[4], device, message);
        } else if (topicInformation[3] === constants.MEASURES_SUFIX) {
            multipleMeasures(apiKey, device, message.toString());
        } else {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                "MEASURES-004: Couldn't process message [%s] due to format issues.",
                /*jshint quotmark: single */
                message
            );
        }
    }

    function processDeviceMeasure(error, device) {
        if (error) {
            config.getLogger().error(context, 'MEASURES-005: Error before processing device measures [%s]', topic);
        } else {
            var localContext = _.clone(context);

            localContext.service = device.service;
            localContext.subservice = device.subservice;

            intoTrans(localContext, processMessageForDevice)(device, apiKey, topicInformation);
        }
    }

    utils.retrieveDevice(deviceId, apiKey, protocol, processDeviceMeasure);
}

/**
 * Handles an incoming AMQP message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the AMQP topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      AMQP message body (Object or Buffer, depending on the value).
 */
function amqpMessageHandler(topic, message) {
    messageHandler(topic, message, 'AMQP');
}

/**
 * Handles an incoming MQTT message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the MQTT topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      MQTT message body (Object or Buffer, depending on the value).
 */
function mqttMessageHandler(topic, message) {
    config.getLogger().debug(context, 'message topic: %s', topic);
    messageHandler(topic, message, 'MQTT');
}

exports.amqpMessageHandler = amqpMessageHandler;
exports.mqttMessageHandler = mqttMessageHandler;
exports.messageHandler = messageHandler;
exports.processMeasureGroup = processMeasureGroup;
exports.guessType = guessType;
<commit_msg>fix topicInformation<commit_after>/*
 * Copyright 2016 Telefonica Investigación y Desarrollo, S.A.U
 *
 * This file is part of iotagent-ul
 *
 * iotagent-ul is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 *
 * iotagent-ul is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public
 * License along with iotagent-ul.
 * If not, seehttp://www.gnu.org/licenses/.
 *
 * For those usages not covered by the GNU Affero General Public License
 * please contact with::[iot_support@tid.es]
 *
 * Modified by: Fernando Méndez, Daniel Calvo - ATOS Research & Innovation
 */

'use strict';

var iotAgentLib = require('iotagent-node-lib'),
    config = require('./configService'),
    transportSelector = require('./transportSelector'),
    intoTrans = iotAgentLib.intoTrans,
    _ = require('underscore'),
    utils = require('./iotaUtils'),
    async = require('async'),
    ulParser = require('./ulParser'),
    constants = require('./constants'),
    context = {
        op: 'IOTAUL.Common.Binding'
    };

/**
 * Find the attribute given by its name between all the active attributes of the given device, returning its type, or
 * null otherwise.
 *
 * @param {String} attribute        Name of the attribute to find.
 * @param {Object} device           Device object containing all the information about a device.
 * @return {String}                 String identifier of the attribute type.
 */
function guessType(attribute, device) {
    for (var i = 0; i < device.active.length; i++) {
        if (device.active[i].name === attribute) {
            return device.active[i].type;
        }
    }

    if (attribute === constants.TIMESTAMP_ATTRIBUTE) {
        if (iotAgentLib.configModule.checkNgsi2()) {
            return constants.TIMESTAMP_TYPE_NGSI2;
        } else {
            return constants.TIMESTAMP_TYPE;
        }
    } else {
        return constants.DEFAULT_ATTRIBUTE_TYPE;
    }
}

function sendConfigurationToDevice(device, apiKey, deviceId, results, callback) {
    transportSelector.applyFunctionFromBinding(
        [apiKey, deviceId, results],
        'sendConfigurationToDevice',
        device.transport,
        callback
    );
}

/**
 * Deals with configuration requests coming from the device. Whenever a new configuration requests arrives with a list
 * of attributes to retrieve, this handler asks the Context Broker for the values of those attributes, and publish a
 * new message in the "/1234/MQTT_2/configuration/values" topic
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} deviceId         Id of the device to be updated.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Object} objMessage          JSON object received.
 */
function manageConfigurationRequest(apiKey, deviceId, device, objMessage) {
    utils.manageConfiguration(
        apiKey,
        deviceId,
        device,
        objMessage,
        async.apply(sendConfigurationToDevice, device),
        function(error) {
            if (error) {
                iotAgentLib.alarms.raise(constants.MQTTB_ALARM, error);
            } else {
                iotAgentLib.alarms.release(constants.MQTTB_ALARM);
                config
                    .getLogger()
                    .debug(context, 'Configuration request finished for APIKey [%s] and Device [%s]', apiKey, deviceId);
            }
        }
    );
}

/**
 * Utility function used to reduce UL Payload group arrays and process them to update their value in the Context Broker.
 * This function process a new element of the payload group, generating a function ready to be called with a callback
 * in a async.series() call.
 *
 * @param {Object} device       Object containing all the information about the device sending the measures.
 * @param {String} apikey       APIKey of the service the device belongs to.
 * @param {Array} previous      Array of prepared functions that send information to the Context Broker.
 * @param {Object} current      Information sent by the device.
 * @param {Number} index        Index of the group in the array.
 * @return {Array}             Updated array of functions.
 */
function processMeasureGroup(device, apikey, previous, current, index) {
    var values = [];

    if (current.command) {
        previous.push(
            iotAgentLib.setCommandResult.bind(
                null,
                device.name,
                config.getConfig().iota.defaultResource,
                apikey,
                current.command,
                current.value,
                constants.COMMAND_STATUS_COMPLETED,
                device
            )
        );
    } else {
        for (var i in current) {
            if (current.hasOwnProperty(i)) {
                values.push({
                    name: i,
                    type: guessType(i, device),
                    value: current[i]
                });
            }
        }

        previous.push(iotAgentLib.update.bind(null, device.name, device.type, '', values, device));
    }

    return previous;
}

/**
 * Adds multiple measures to the Context Broker. Multiple measures come in the form of single-level JSON objects,
 * whose keys are the attribute names and whose values are the attribute values.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {String} message          UL payload.
 */
function multipleMeasures(apiKey, device, message) {
    var updates = [],
        messageStr = message.toString(),
        parsedMessage;

    config.getLogger().debug('Processing multiple measures for device [%s] with apiKey [%s]', device.id, apiKey);

    try {
        parsedMessage = ulParser.parse(messageStr);
    } catch (e) {
        config.getLogger().error(context, 'MEASURES-003: Parse error parsing incoming message [%]', messageStr);
        return;
    }

    updates = parsedMessage.reduce(processMeasureGroup.bind(null, device, apiKey), []);

    async.series(updates, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Multiple measures for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Adds a single measure to the context broker. The message for single measures contains the direct value to
 * be inserted in the attribute, given by its name.
 *
 * @param {String} apiKey           API Key corresponding to the Devices configuration.
 * @param {String} attribute        Name of the attribute to update.
 * @param {Object} device           Device object containing all the information about a device.
 * @param {Buffer} message          Raw message coming from the client.
 */
function singleMeasure(apiKey, attribute, device, message) {
    var values;

    config.getLogger().debug('Processing single measure for device [%s] with apiKey [%s]', device.id, apiKey);

    values = [
        {
            name: attribute,
            type: guessType(attribute, device),
            value: message.toString()
        }
    ];

    iotAgentLib.update(device.name, device.type, '', values, device, function(error) {
        if (error) {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                " MEASURES-002: Couldn't send the updated values to the Context Broker due to an error: %s",
                /*jshint quotmark: single */
                error
            );
        } else {
            // prettier-ignore
            config.getLogger().debug(
                context,
                'Single measure for device [%s] with apiKey [%s] successfully updated',
                device.id,
                apiKey
            );
        }
    });
}

/**
 * Handles an incoming message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      message body (Object or Buffer, depending on the value).
 */
function messageHandler(topic, message, protocol) {
    var topicInformation = topic.split('/'),
        apiKey,
        deviceId,
        messageStr,
        parsedMessage;

    if ('ul' === topicInformation[1].toLowerCase()) {
        topicInformation.splice(1, 1);
    }
    apiKey = topicInformation[1];
    deviceId = topicInformation[2];
    messageStr = message.toString();

    function processMessageForDevice(device, apiKey, topicInformation) {
        iotAgentLib.alarms.release(constants.MQTTB_ALARM);

        if (
            topicInformation[3] === constants.CONFIGURATION_SUFIX &&
            topicInformation[4] === constants.CONFIGURATION_COMMAND_SUFIX &&
            message
        ) {
            parsedMessage = ulParser.parseConfigurationRequest(messageStr);
            manageConfigurationRequest(apiKey, deviceId, device, parsedMessage);
        } else if (topicInformation[3] === constants.CONFIGURATION_COMMAND_UPDATE) {
            var commandObj = ulParser.result(message.toString());
            utils.updateCommand(
                apiKey,
                device,
                commandObj.result,
                commandObj.command,
                constants.COMMAND_STATUS_COMPLETED,
                function(error) {
                    config.getLogger().debug('Command updated with result: %s', error);
                }
            );
        } else if (topicInformation[4]) {
            singleMeasure(apiKey, topicInformation[4], device, message);
        } else if (topicInformation[3] === constants.MEASURES_SUFIX) {
            multipleMeasures(apiKey, device, message.toString());
        } else {
            config.getLogger().error(
                context,
                /*jshint quotmark: double */
                "MEASURES-004: Couldn't process message [%s] due to format issues.",
                /*jshint quotmark: single */
                message
            );
        }
    }

    function processDeviceMeasure(error, device) {
        if (error) {
            config.getLogger().error(context, 'MEASURES-005: Error before processing device measures [%s]', topic);
        } else {
            var localContext = _.clone(context);

            localContext.service = device.service;
            localContext.subservice = device.subservice;

            intoTrans(localContext, processMessageForDevice)(device, apiKey, topicInformation);
        }
    }

    utils.retrieveDevice(deviceId, apiKey, protocol, processDeviceMeasure);
}

/**
 * Handles an incoming AMQP message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the AMQP topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      AMQP message body (Object or Buffer, depending on the value).
 */
function amqpMessageHandler(topic, message) {
    messageHandler(topic, message, 'AMQP');
}

/**
 * Handles an incoming MQTT message, extracting the API Key, device Id and attribute to update (in the case of single
 * measures) from the MQTT topic.
 *
 * @param {String} topic        Topic of the form: '/<APIKey>/deviceId/attributes[/<attributeName>]'.
 * @param {Object} message      MQTT message body (Object or Buffer, depending on the value).
 */
function mqttMessageHandler(topic, message) {
    config.getLogger().debug(context, 'message topic: %s', topic);
    messageHandler(topic, message, 'MQTT');
}

exports.amqpMessageHandler = amqpMessageHandler;
exports.mqttMessageHandler = mqttMessageHandler;
exports.messageHandler = messageHandler;
exports.processMeasureGroup = processMeasureGroup;
exports.guessType = guessType;
<|endoftext|>


<commit_before>var request = require('request')
  , vows = require('vows')
  , assert = require('assert')
  , captureRE = /\[([A-Z_]+)\]/

function convertRequestHeaders(arr, sub) {
    var headers = {}, match

    for (var i = 0; i < arr.length; i++) {
        if (!(match = /([^:]+):[ \t]+(.*)/.exec(arr[i]))) continue

        if (sub)
            headers[match[1].toLowerCase()] = substitute(match[2])
        else
            headers[match[1].toLowerCase()] = match[2]
    }

    return headers
}

var api = {}

api.vars = {}

api.call = function (params) {
   return function() {
        var self = this
          , options =
            { uri: api.root + substitute(params.path)
            , method: params.method
            , headers: convertRequestHeaders(params.headers, true)
            }

        if (params.body) options.body = substitute(params.body, true)

        request(options, function (error, response, body) {
            self.callback(error, response)
        })
    }
}

function substitute(string, json) {
    for (var key in api.vars) {
        if (!api.vars.hasOwnProperty(key)) continue
        if (json)
            string = string.replace('[' + key + ']', JSON.stringify(api.vars[key]))
        else
            string = string.replace('[' + key + ']', api.vars[key])
    }
    return string
}

function substituteResponse(string, json) {
    for (var key in api.vars) {
        if (!api.vars.hasOwnProperty(key)) continue
        if (json)
            string = string.replace('[' + key + ']', JSON.stringify(api.vars[key]))
        else
            string = string.replace('[' + key + ']', api.vars[key])
    }

    return string
}

function headerCapture(string) {
    var captures = []
      , regex = string.replace(captureRE, function(m, name) { captures.push(name); return '(.*)' })

    if (captures.length > 0) return { regex: new RegExp(regex, 'g'), captures: captures }
}

function jsonCompare(sample, example, path) {
    var ok = true
    path = path || []

    function test(a, b, message) {
        assert.equal(a, b, "At key " + path.join('.') + ": expected " + a + ", got " + b)
    }

    if ('object' == typeof example && example) {
        if (example.hasOwnProperty('$CAPTURE')) {
            api.vars[example.$CAPTURE] = sample
        } else {
            test(typeof example, typeof sample)

            for (var key in example) {
                if (!example.hasOwnProperty(key)) continue
                jsonCompare(sample[key], example[key], path.concat(key))
            }
        }
    } else {
        test(typeof example, typeof sample)
        test(example, sample)
    }
}

api.matchers = function (batch, params) {
    batch[params.label] = function(error, result) {
        assert.equal(result.statusCode, params.code, "Got status code: " + result.statusCode)
    }

    var headers = convertRequestHeaders(params.headers)
    for (var key in headers) {
        batch[key + ": " + headers[key]] = function(error, result) {
            var pattern, header = substitute(headers[key])
            if (pattern = headerCapture(header)) {
                assert.ok(result.headers[key], "Header should exist, but doesn't")
                var match = pattern.regex.exec(result.headers[key])
                assert.ok(match, "Header did not match pattern")
                for (var i = 0; i < pattern.captures.length; i++) {
                    assert.ok(match[i], pattern.captures.length, "Header did not match all patterns")
                    api.vars[pattern.captures[i]] = match[i + 1]
                }
            } else {
                assert.equal(result.headers[key], headers[key])
            }
        }
    }

    if (params.body) {
        batch[params.body.replace(/\s+/g, " ")] = function(error, result) {
            var pattern = substitute(params.body, true)
            pattern = pattern.replace(captureRE, '{ "$CAPTURE": "$1" }')
            pattern = JSON.parse(pattern)

            assert.notEqual(result.body, '', "No body returned")
            jsonCompare(JSON.parse(result.body), pattern)
        }
    } else {
        batch["Empty response"] = function(error, result) {
            assert.equal(result.body, '', "Body should be empty")
        }
    }
}

api.makeBatch = function (test) {
    var batch = {}, topic = {}

    batch[test.req.label] = topic = { topic: api.call(test.req) }
    if (test.resp)
        api.matchers(topic, test.resp);
    else
        topic['done'] = function(err, result) { assert.ok(result) }

    return batch;
}

api.compile = function (results) {
    var suite = vows.describe(results.title)

    for (var i = 0; i < results.tests.length; i++) {
        suite.addBatch(api.makeBatch(results.tests[i]))
    }

    return suite
}

module.exports = api

<commit_msg>Capture all references<commit_after>var request = require('request')
  , vows = require('vows')
  , assert = require('assert')
  , captureRE = /\[([A-Z_]+)\]/g

function convertRequestHeaders(arr, sub) {
    var headers = {}, match

    for (var i = 0; i < arr.length; i++) {
        if (!(match = /([^:]+):[ \t]+(.*)/.exec(arr[i]))) continue

        if (sub)
            headers[match[1].toLowerCase()] = substitute(match[2])
        else
            headers[match[1].toLowerCase()] = match[2]
    }

    return headers
}

var api = {}

api.vars = {}

api.call = function (params) {
   return function() {
        var self = this
          , options =
            { uri: api.root + substitute(params.path)
            , method: params.method
            , headers: convertRequestHeaders(params.headers, true)
            }

        if (params.body) options.body = substitute(params.body, true)

        request(options, function (error, response, body) {
            self.callback(error, response)
        })
    }
}

function substitute(string, json) {
    for (var key in api.vars) {
        if (!api.vars.hasOwnProperty(key)) continue
        if (json)
            string = string.replace('[' + key + ']', JSON.stringify(api.vars[key]))
        else
            string = string.replace('[' + key + ']', api.vars[key])
    }
    return string
}

function substituteResponse(string, json) {
    for (var key in api.vars) {
        if (!api.vars.hasOwnProperty(key)) continue
        if (json)
            string = string.replace('[' + key + ']', JSON.stringify(api.vars[key]))
        else
            string = string.replace('[' + key + ']', api.vars[key])
    }

    return string
}

function headerCapture(string) {
    var captures = []
      , regex = string.replace(captureRE, function(m, name) { captures.push(name); return '(.*)' })

    if (captures.length > 0) return { regex: new RegExp(regex, 'g'), captures: captures }
}

function jsonCompare(sample, example, path) {
    var ok = true
    path = path || []

    function test(a, b, message) {
        assert.equal(a, b, "At key " + path.join('.') + ": expected " + a + ", got " + b)
    }

    if ('object' == typeof example && example) {
        if (example.hasOwnProperty('$CAPTURE')) {
            api.vars[example.$CAPTURE] = sample
        } else {
            test(typeof example, typeof sample)

            for (var key in example) {
                if (!example.hasOwnProperty(key)) continue
                jsonCompare(sample[key], example[key], path.concat(key))
            }
        }
    } else {
        test(typeof example, typeof sample)
        test(example, sample)
    }
}

api.matchers = function (batch, params) {
    batch[params.label] = function(error, result) {
        assert.equal(result.statusCode, params.code, "Got status code: " + result.statusCode)
    }

    var headers = convertRequestHeaders(params.headers)
    for (var key in headers) {
        batch[key + ": " + headers[key]] = function(error, result) {
            var pattern, header = substitute(headers[key])
            if (pattern = headerCapture(header)) {
                assert.ok(result.headers[key], "Header should exist, but doesn't")
                var match = pattern.regex.exec(result.headers[key])
                assert.ok(match, "Header did not match pattern")
                for (var i = 0; i < pattern.captures.length; i++) {
                    assert.ok(match[i], pattern.captures.length, "Header did not match all patterns")
                    api.vars[pattern.captures[i]] = match[i + 1]
                }
            } else {
                assert.equal(result.headers[key], headers[key])
            }
        }
    }

    if (params.body) {
        batch[params.body.replace(/\s+/g, " ")] = function(error, result) {
            var pattern = substitute(params.body, true)
            pattern = pattern.replace(captureRE, '{ "$CAPTURE": "$1" }')
            pattern = JSON.parse(pattern)

            assert.notEqual(result.body, '', "No body returned")
            jsonCompare(JSON.parse(result.body), pattern)
        }
    } else {
        batch["Empty response"] = function(error, result) {
            assert.equal(result.body, '', "Body should be empty")
        }
    }
}

api.makeBatch = function (test) {
    var batch = {}, topic = {}

    batch[test.req.label] = topic = { topic: api.call(test.req) }
    if (test.resp)
        api.matchers(topic, test.resp);
    else
        topic['done'] = function(err, result) { assert.ok(result) }

    return batch;
}

api.compile = function (results) {
    var suite = vows.describe(results.title)

    for (var i = 0; i < results.tests.length; i++) {
        suite.addBatch(api.makeBatch(results.tests[i]))
    }

    return suite
}

module.exports = api

<|endoftext|>


<commit_before>var React = require('react/addons');
var Immutable = require('immutable');
var ContentFinder = require('../modules/ContentFinder');
var cx = require("classnames");

require('../stylesheets/Bar.scss');

var cloneWithProps = React.addons.cloneWithProps;
var { object, instanceOf } = React.PropTypes;

var Bar = React.createClass({
  propTypes: {
    content: instanceOf(Immutable.Map),
    selection: object.isRequired,
  },

  getInitialState() {
    return { open: false };
  },

  // collapse buttons when inactive
  componentWillUpdate() {
    if (!this.isActive() && this.state.open) {
      this.setState({open: false});
    }
  },

  componentDidMount() {
    this.recalculatePosition();
  },

  componentDidUpdate() {
    this.recalculatePosition();
  },

  // update position based on the selected anchor node
  recalculatePosition() {
    if (!this.isActive()) { return; }

    var selection = this.props.selection;
    var guid = selection.guids().anchor;
    var node = document.getElementsByName(guid)[0];

    var rect = node.getBoundingClientRect();
    var scrollTop =  window.pageYOffset || document.documentElement.scrollTop;

    var node = React.findDOMNode(this);
    node.style.top  = rect.top + scrollTop - 5 + "px";
    node.style.left = rect.left - 70 + "px";
  },

  // content is empty and a new paragraph block
  isActive() {
    var selection = this.props.selection;
    var isRange = selection.isRange();
    var isBeg   = selection.begOfBlock();
    if (isRange || !isBeg) { return false; }

    var guid = selection.guids().anchor;
    var path = new ContentFinder(this.props.content).findPath(guid);
    var block = this.props.content.getIn(path);
    return block.get('text') === '' && block.get('type') === 'p';
  },

  // switch to allow input if button needs a value
  handleSetValue(obj, ref) {
    this.valueButton = this.refs[ref];
    this.setState({inputMode: true});
  },

  // toggle menu open/closed
  handleClickTrigger(e) {
    var open = this.state.open;
    this.setState({open: !open});

    // focus on the button
    if (!open) {
      setTimeout( () => {
        React.findDOMNode(this.refs.trigger).focus();
      }, 100);
    }
  },

  // switch to allow input if button needs a value
  handleSetValue(obj, ref) {
    this.valueButton = this.refs[ref];
    this.setState({inputMode: true});
  },

  // send click with value when we press return
  handleOnKeyUp(e) {
    var returnKey = 13;
    if (e.keyCode !== returnKey) { return; }

    var value = e.target.value;
    this.valueButton.handleClick(e, null, value);
    React.findDOMNode(this.refs.linkInput).value = "";
    this.setState({inputMode: false});
  },

  handleCancelInput() {
    this.setState({inputMode: false});
  },

  // class names
  barClasses() {
    return cx({
      'ic-Editor-Bar': true,
      'ic-Editor-Bar--active': this.isActive()
    });
  },

  triggerClasses() {
    return cx({
      'ic-Editor-Bar__trigger': true,
      'ic-Editor-Bar__trigger--active': this.state.open
    });
  },

  iconClasses() {
    return cx({
      'ic-Editor-Bar__trigger-icon fa fa-plus': true,
      'ic-Editor-Bar__trigger-icon--active': this.state.open
    });
  },

  menuClasses() {
    return cx({
      'ic-Editor-Bar__menu-items': true,
      'ic-Editor-Bar__menu-items--active': this.state.open
    });
  },

  renderButtons() {
    var buttons = [];
    React.Children.forEach(this.props.children, (child, i) => {
      var refName = `button_${child.props.type}`;
      var cloned = cloneWithProps(child, {
        content: this.props.content,
        selection: this.props.selection,
        handleSetValue: this.handleSetValue.bind(this, child, refName),
        ref: refName
      });

      buttons.push(
        <li className="ic-Editor-Bar__menu-item" key={`button_${i}`}>
          {cloned}

      );
    });
    return buttons;
  },

  render() {
    return (

        <button ref="trigger"
          className={this.triggerClasses()}
          onClick={this.handleClickTrigger}
        >

          Show Menu
        </button>

        {this.isActive() &&

            {this.renderButtons()}

        }

    );
  }
});

module.exports = Bar;
<commit_msg>bump down timeout<commit_after>var React = require('react/addons');
var Immutable = require('immutable');
var ContentFinder = require('../modules/ContentFinder');
var cx = require("classnames");

require('../stylesheets/Bar.scss');

var cloneWithProps = React.addons.cloneWithProps;
var { object, instanceOf } = React.PropTypes;

var Bar = React.createClass({
  propTypes: {
    content: instanceOf(Immutable.Map),
    selection: object.isRequired,
  },

  getInitialState() {
    return { open: false };
  },

  // collapse buttons when inactive
  componentWillUpdate() {
    if (!this.isActive() && this.state.open) {
      this.setState({open: false});
    }
  },

  componentDidMount() {
    this.recalculatePosition();
  },

  componentDidUpdate() {
    this.recalculatePosition();
  },

  // update position based on the selected anchor node
  recalculatePosition() {
    if (!this.isActive()) { return; }

    var selection = this.props.selection;
    var guid = selection.guids().anchor;
    var node = document.getElementsByName(guid)[0];

    var rect = node.getBoundingClientRect();
    var scrollTop =  window.pageYOffset || document.documentElement.scrollTop;

    var node = React.findDOMNode(this);
    node.style.top  = rect.top + scrollTop - 5 + "px";
    node.style.left = rect.left - 70 + "px";
  },

  // content is empty and a new paragraph block
  isActive() {
    var selection = this.props.selection;
    var isRange = selection.isRange();
    var isBeg   = selection.begOfBlock();
    if (isRange || !isBeg) { return false; }

    var guid = selection.guids().anchor;
    var path = new ContentFinder(this.props.content).findPath(guid);
    var block = this.props.content.getIn(path);
    return block.get('text') === '' && block.get('type') === 'p';
  },

  // switch to allow input if button needs a value
  handleSetValue(obj, ref) {
    this.valueButton = this.refs[ref];
    this.setState({inputMode: true});
  },

  // toggle menu open/closed
  handleClickTrigger(e) {
    var open = this.state.open;
    this.setState({open: !open});

    // focus on the button
    if (!open) {
      setTimeout( () => {
        React.findDOMNode(this.refs.trigger).focus();
      }, 50);
    }
  },

  // switch to allow input if button needs a value
  handleSetValue(obj, ref) {
    this.valueButton = this.refs[ref];
    this.setState({inputMode: true});
  },

  // send click with value when we press return
  handleOnKeyUp(e) {
    var returnKey = 13;
    if (e.keyCode !== returnKey) { return; }

    var value = e.target.value;
    this.valueButton.handleClick(e, null, value);
    React.findDOMNode(this.refs.linkInput).value = "";
    this.setState({inputMode: false});
  },

  handleCancelInput() {
    this.setState({inputMode: false});
  },

  // class names
  barClasses() {
    return cx({
      'ic-Editor-Bar': true,
      'ic-Editor-Bar--active': this.isActive()
    });
  },

  triggerClasses() {
    return cx({
      'ic-Editor-Bar__trigger': true,
      'ic-Editor-Bar__trigger--active': this.state.open
    });
  },

  iconClasses() {
    return cx({
      'ic-Editor-Bar__trigger-icon fa fa-plus': true,
      'ic-Editor-Bar__trigger-icon--active': this.state.open
    });
  },

  menuClasses() {
    return cx({
      'ic-Editor-Bar__menu-items': true,
      'ic-Editor-Bar__menu-items--active': this.state.open
    });
  },

  renderButtons() {
    var buttons = [];
    React.Children.forEach(this.props.children, (child, i) => {
      var refName = `button_${child.props.type}`;
      var cloned = cloneWithProps(child, {
        content: this.props.content,
        selection: this.props.selection,
        handleSetValue: this.handleSetValue.bind(this, child, refName),
        ref: refName
      });

      buttons.push(
        <li className="ic-Editor-Bar__menu-item" key={`button_${i}`}>
          {cloned}

      );
    });
    return buttons;
  },

  render() {
    return (

        <button ref="trigger"
          className={this.triggerClasses()}
          onClick={this.handleClickTrigger}
        >

          Show Menu
        </button>

        {this.isActive() &&

            {this.renderButtons()}

        }

    );
  }
});

module.exports = Bar;
<|endoftext|>


<commit_before>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.borders',
  name: 'SplitScreenBorder',
  extends: 'foam.u2.Element',

  css: `
    ^ {
      height: 100vh;
      width: 100vw;
      background: #fff;
    }
    ^ .left-block {
      width: 55vw;
      display: inline-block;
      background: transparent;
      text-align: center;
    }
    ^ .right-block {
      width: 43vw;
      display: inline-block;
      background: transparent;
      height: auto;
      float: right;
    }
    ^content {
      width: -moz-available;
      width: -webkit-fill-available;
      width: fill-available;
      position: relative;
      padding-bottom: 4vh;
    }
    ^ .wrapper-outer {
      overflow: auto;
      height: 100%;
    }
`,

properties: [
  'leftPanel',
  'rightPanel'
],

methods: [
  function init() {
    this.addClass(this.myClass())
    .start().addClass('wrapper-outer')
        .start().addClass('left-block')
          .start('div', null, this.leftPanel$)
              .addClass(this.myClass('content'))
          .end()
        .end()
        .start().addClass('right-block')
          .start('div', null, this.rightPanel$)
            .addClass(this.myClass('content'))
          .end()
        .end()
    .end();
  }
]
});
<commit_msg>Formatting.<commit_after>/**
 * @license
 * Copyright 2020 The FOAM Authors. All Rights Reserved.
 * http://www.apache.org/licenses/LICENSE-2.0
 */

foam.CLASS({
  package: 'foam.u2.borders',
  name: 'SplitScreenBorder',
  extends: 'foam.u2.Element',

  css: `
    ^ {
      height: 100vh;
      width: 100vw;
      background: #fff;
    }
    ^ .left-block {
      width: 55vw;
      display: inline-block;
      background: transparent;
      text-align: center;
    }
    ^ .right-block {
      width: 43vw;
      display: inline-block;
      background: transparent;
      height: auto;
      float: right;
    }
    ^content {
      width: -moz-available;
      width: -webkit-fill-available;
      width: fill-available;
      position: relative;
      padding-bottom: 4vh;
    }
    ^ .wrapper-outer {
      overflow: auto;
      height: 100%;
    }
  `,

  properties: [
    'leftPanel',
    'rightPanel'
  ],

  methods: [
    function init() {
      this.addClass(this.myClass())
      .start().addClass('wrapper-outer')
        .start().addClass('left-block')
          .start('div', null, this.leftPanel$)
              .addClass(this.myClass('content'))
          .end()
        .end()
        .start().addClass('right-block')
          .start('div', null, this.rightPanel$)
            .addClass(this.myClass('content'))
          .end()
        .end()
      .end();
    }
  ]
});
<|endoftext|>


<commit_before>"use strict";

var CityTour = CityTour || {};

CityTour.RoadNetworkGenerator = (function() {
  var DISTANCE_TO_CITY_EDGE = Math.min(CityTour.Config.HALF_BLOCK_COLUMNS, CityTour.Config.HALF_BLOCK_ROWS);
  var MAX_STEEPNESS = Math.PI / 6;
  var MAX_BRIDGE_LENGTH = 10;

  var buildRoadNetwork = function(terrain, config) {
    var centerMapX = config.centerMapX;
    var centerMapZ = config.centerMapZ;

    var MIN_MAP_X = -CityTour.Config.HALF_BLOCK_COLUMNS + centerMapX;
    var MAX_MAP_X = CityTour.Config.HALF_BLOCK_COLUMNS + centerMapX;
    var MIN_MAP_Z = -CityTour.Config.HALF_BLOCK_ROWS + centerMapZ;
    var MAX_MAP_Z = CityTour.Config.HALF_BLOCK_ROWS + centerMapZ;

    var SAFE_FROM_DECAY_DISTANCE = DISTANCE_TO_CITY_EDGE * config.safeFromDecayPercentage;

    var probabilityOfBranching = function(mapX1, mapZ1, mapX2, mapZ2) {
      // Guarantee roads along x and z axes
      if (mapX1 === centerMapX && mapX2 === centerMapX && mapZ2 >= MIN_MAP_Z && mapZ2 <= MAX_MAP_Z) {
        return 1.0;
      }
      else if (mapZ1 === centerMapZ && mapZ2 === centerMapZ && mapX2 >= MIN_MAP_X && mapX2 <= MAX_MAP_X) {
        return 1.0;
      }

      var xDistance = mapX1 - centerMapX;
      var zDistance = mapZ1 - centerMapZ;
      var distanceFromCenter = Math.sqrt((xDistance * xDistance) + (zDistance * zDistance));
      var normalizedPercentageFromCenter;

      if (distanceFromCenter > SAFE_FROM_DECAY_DISTANCE) {
        normalizedPercentageFromCenter = (distanceFromCenter - SAFE_FROM_DECAY_DISTANCE) / (DISTANCE_TO_CITY_EDGE - SAFE_FROM_DECAY_DISTANCE);
      }
      else {
        normalizedPercentageFromCenter = 0.0;
      }

      return (Math.pow(0.5, normalizedPercentageFromCenter) - 0.5) * 2;
    };

    var isTerrainTooSteep = function(terrain, mapX, mapZ, targetMapX, targetMapZ) {
      var heightAtPoint1 = terrain.heightAtCoordinates(mapX, mapZ);
      var heightAtPoint2 = terrain.heightAtCoordinates(targetMapX, targetMapZ);
      var angle = Math.atan2((heightAtPoint1 - heightAtPoint2), CityTour.Config.BLOCK_DEPTH);

      return Math.abs(angle) > MAX_STEEPNESS;
    };

    var shouldConnectIntersections = function(terrain, mapX1, mapZ1, mapX2, mapZ2) {
      var edgeIsOnLand = terrain.materialAtCoordinates(mapX1, mapZ1) === CityTour.Terrain.LAND &&
                         terrain.materialAtCoordinates(mapX2, mapZ2) === CityTour.Terrain.LAND;

      return edgeIsOnLand &&
             (Math.random() < probabilityOfBranching(mapX1, mapZ1, mapX2, mapZ2)) &&
             !isTerrainTooSteep(terrain, mapX1, mapZ1, mapX2, mapZ2);
    };

    var branchFromIntersection = function(terrain, roadNetwork, mapX, mapZ) {
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX - 1, mapZ);
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX, mapZ - 1);
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX + 1, mapZ);
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX, mapZ + 1);
    };

    var calculateBridgeAttributes = function(terrain, roadNetwork, mapX, mapZ, targetMapX, targetMapZ) {
      var xDistance = mapX - centerMapX;
      var zDistance = mapZ - centerMapZ;
      var distanceFromCenter = Math.sqrt((xDistance * xDistance) + (zDistance * zDistance));
      if (distanceFromCenter > SAFE_FROM_DECAY_DISTANCE) {
        return null;
      }

      var xDelta, zDelta;
      if (targetMapX === mapX) {
        xDelta = 0.0;
      }
      else {
        xDelta = (targetMapX < mapX) ? -1 : 1;
      }
      if (targetMapZ === mapZ) {
        zDelta = 0.0;
      }
      else {
        zDelta = (targetMapZ < mapZ) ? -1 : 1;
      }
      var finalX = targetMapX;
      var finalZ = targetMapZ;

      var bridgeLength = 1;
      while (terrain.materialAtCoordinates(finalX, finalZ) === CityTour.Terrain.WATER) {
        if (roadNetwork.hasIntersection(finalX, finalZ)) {
          return null;
        }

        finalX += xDelta;
        finalZ += zDelta;
        bridgeLength += 1;

        if (finalX < -CityTour.Config.HALF_TERRAIN_COLUMNS ||
            finalX > CityTour.Config.HALF_TERRAIN_COLUMNS  ||
            finalZ < -CityTour.Config.HALF_TERRAIN_ROWS    ||
            finalZ > CityTour.Config.HALF_TERRAIN_ROWS) {
          return null;
        }
      }

      if (bridgeLength > MAX_BRIDGE_LENGTH) {
        return null;
      }

      var heightAtTerminal1 = terrain.heightAtCoordinates(mapX, mapZ);
      var heightAtTerminal2 = terrain.heightAtCoordinates(finalX, finalZ);
      if (heightAtTerminal1 !== heightAtTerminal2) {
        return null;
      }
      var roadDeckHeight = Math.max(heightAtTerminal1, heightAtTerminal2);

      if (Math.random() < 0.5) {
        return null;
      }

      return {
        roadDeckHeight: roadDeckHeight,
        endX: finalX,
        endZ: finalZ,
        xDelta: xDelta,
        zDelta: zDelta,
      };
    };

    var connectIntersections = function(terrain, roadNetwork, mapX, mapZ, targetMapX, targetMapZ) {
      if (terrain.materialAtCoordinates(targetMapX, targetMapZ) === CityTour.Terrain.WATER) {
        var bridgeAttributes = calculateBridgeAttributes(terrain, roadNetwork, mapX, mapZ, targetMapX, targetMapZ);

        if (bridgeAttributes !== null) {
          var tempX = mapX;
          var tempZ = mapZ;
          while (tempX < bridgeAttributes.endX || tempZ < bridgeAttributes.endZ) {
            roadNetwork.addEdge(tempX,
                                tempZ,
                                tempX + bridgeAttributes.xDelta,
                                tempZ + bridgeAttributes.zDelta,
                                bridgeAttributes.roadDeckHeight,
                                CityTour.RoadNetwork.BRIDGE_SURFACE);
            tempX += bridgeAttributes.xDelta;
            tempZ += bridgeAttributes.zDelta;
          }

          branchFromIntersection(terrain, roadNetwork, bridgeAttributes.endX, bridgeAttributes.endZ);
        }
      }
      else {
        if (shouldConnectIntersections(terrain, mapX, mapZ, targetMapX, targetMapZ)) {
          if (roadNetwork.hasIntersection(targetMapX, targetMapZ)) {
            roadNetwork.addEdge(mapX, mapZ, targetMapX, targetMapZ, 0.0, CityTour.RoadNetwork.TERRAIN_SURFACE);
          }
          else {
            roadNetwork.addEdge(mapX, mapZ, targetMapX, targetMapZ, 0.0, CityTour.RoadNetwork.TERRAIN_SURFACE);
            branchFromIntersection(terrain, roadNetwork, targetMapX, targetMapZ);
          }
        }
      }
    };

    var roadNetwork = new CityTour.RoadNetwork(terrain);
    branchFromIntersection(terrain, roadNetwork, centerMapX, centerMapZ);

    return roadNetwork;
  };

  var roadNetworkGenerator = {};

  roadNetworkGenerator.generate = function(terrain, config) {
    return buildRoadNetwork(terrain, config);
  };

  return roadNetworkGenerator;
})();
<commit_msg>Better variable names<commit_after>"use strict";

var CityTour = CityTour || {};

CityTour.RoadNetworkGenerator = (function() {
  var DISTANCE_TO_CITY_EDGE = Math.min(CityTour.Config.HALF_BLOCK_COLUMNS, CityTour.Config.HALF_BLOCK_ROWS);
  var MAX_STEEPNESS = Math.PI / 6;
  var MAX_BRIDGE_LENGTH = 10;

  var buildRoadNetwork = function(terrain, config) {
    var centerMapX = config.centerMapX;
    var centerMapZ = config.centerMapZ;

    var MIN_MAP_X = -CityTour.Config.HALF_BLOCK_COLUMNS + centerMapX;
    var MAX_MAP_X = CityTour.Config.HALF_BLOCK_COLUMNS + centerMapX;
    var MIN_MAP_Z = -CityTour.Config.HALF_BLOCK_ROWS + centerMapZ;
    var MAX_MAP_Z = CityTour.Config.HALF_BLOCK_ROWS + centerMapZ;

    var SAFE_FROM_DECAY_DISTANCE = DISTANCE_TO_CITY_EDGE * config.safeFromDecayPercentage;

    var probabilityOfBranching = function(mapX1, mapZ1, mapX2, mapZ2) {
      // Guarantee roads along x and z axes
      if (mapX1 === centerMapX && mapX2 === centerMapX && mapZ2 >= MIN_MAP_Z && mapZ2 <= MAX_MAP_Z) {
        return 1.0;
      }
      else if (mapZ1 === centerMapZ && mapZ2 === centerMapZ && mapX2 >= MIN_MAP_X && mapX2 <= MAX_MAP_X) {
        return 1.0;
      }

      var xDistance = mapX1 - centerMapX;
      var zDistance = mapZ1 - centerMapZ;
      var distanceFromCenter = Math.sqrt((xDistance * xDistance) + (zDistance * zDistance));
      var normalizedPercentageFromCenter;

      if (distanceFromCenter > SAFE_FROM_DECAY_DISTANCE) {
        normalizedPercentageFromCenter = (distanceFromCenter - SAFE_FROM_DECAY_DISTANCE) / (DISTANCE_TO_CITY_EDGE - SAFE_FROM_DECAY_DISTANCE);
      }
      else {
        normalizedPercentageFromCenter = 0.0;
      }

      return (Math.pow(0.5, normalizedPercentageFromCenter) - 0.5) * 2;
    };

    var isTerrainTooSteep = function(terrain, mapX, mapZ, targetMapX, targetMapZ) {
      var heightAtPoint1 = terrain.heightAtCoordinates(mapX, mapZ);
      var heightAtPoint2 = terrain.heightAtCoordinates(targetMapX, targetMapZ);
      var angle = Math.atan2((heightAtPoint1 - heightAtPoint2), CityTour.Config.BLOCK_DEPTH);

      return Math.abs(angle) > MAX_STEEPNESS;
    };

    var shouldConnectIntersections = function(terrain, mapX1, mapZ1, mapX2, mapZ2) {
      var edgeIsOnLand = terrain.materialAtCoordinates(mapX1, mapZ1) === CityTour.Terrain.LAND &&
                         terrain.materialAtCoordinates(mapX2, mapZ2) === CityTour.Terrain.LAND;

      return edgeIsOnLand &&
             (Math.random() < probabilityOfBranching(mapX1, mapZ1, mapX2, mapZ2)) &&
             !isTerrainTooSteep(terrain, mapX1, mapZ1, mapX2, mapZ2);
    };

    var branchFromIntersection = function(terrain, roadNetwork, mapX, mapZ) {
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX - 1, mapZ);
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX, mapZ - 1);
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX + 1, mapZ);
      connectIntersections(terrain, roadNetwork, mapX, mapZ, mapX, mapZ + 1);
    };

    var calculateBridgeAttributes = function(terrain, roadNetwork, mapX, mapZ, targetMapX, targetMapZ) {
      var xDistanceFromCenter = mapX - centerMapX;
      var zDistanceFromCenter = mapZ - centerMapZ;
      var distanceFromCenter = Math.sqrt((xDistanceFromCenter * xDistanceFromCenter) + (zDistanceFromCenter * zDistanceFromCenter));
      if (distanceFromCenter > SAFE_FROM_DECAY_DISTANCE) {
        return null;
      }

      var xDelta, zDelta;
      if (targetMapX === mapX) {
        xDelta = 0.0;
      }
      else {
        xDelta = (targetMapX < mapX) ? -1 : 1;
      }
      if (targetMapZ === mapZ) {
        zDelta = 0.0;
      }
      else {
        zDelta = (targetMapZ < mapZ) ? -1 : 1;
      }
      var finalX = targetMapX;
      var finalZ = targetMapZ;

      var bridgeLength = 1;
      while (terrain.materialAtCoordinates(finalX, finalZ) === CityTour.Terrain.WATER) {
        if (roadNetwork.hasIntersection(finalX, finalZ)) {
          return null;
        }

        finalX += xDelta;
        finalZ += zDelta;
        bridgeLength += 1;

        if (finalX < -CityTour.Config.HALF_TERRAIN_COLUMNS ||
            finalX > CityTour.Config.HALF_TERRAIN_COLUMNS  ||
            finalZ < -CityTour.Config.HALF_TERRAIN_ROWS    ||
            finalZ > CityTour.Config.HALF_TERRAIN_ROWS) {
          return null;
        }
      }

      if (bridgeLength > MAX_BRIDGE_LENGTH) {
        return null;
      }

      var heightAtTerminal1 = terrain.heightAtCoordinates(mapX, mapZ);
      var heightAtTerminal2 = terrain.heightAtCoordinates(finalX, finalZ);
      if (heightAtTerminal1 !== heightAtTerminal2) {
        return null;
      }
      var roadDeckHeight = Math.max(heightAtTerminal1, heightAtTerminal2);

      if (Math.random() < 0.5) {
        return null;
      }

      return {
        roadDeckHeight: roadDeckHeight,
        endX: finalX,
        endZ: finalZ,
        xDelta: xDelta,
        zDelta: zDelta,
      };
    };

    var connectIntersections = function(terrain, roadNetwork, mapX, mapZ, targetMapX, targetMapZ) {
      if (terrain.materialAtCoordinates(targetMapX, targetMapZ) === CityTour.Terrain.WATER) {
        var bridgeAttributes = calculateBridgeAttributes(terrain, roadNetwork, mapX, mapZ, targetMapX, targetMapZ);

        if (bridgeAttributes !== null) {
          var tempX = mapX;
          var tempZ = mapZ;
          while (tempX < bridgeAttributes.endX || tempZ < bridgeAttributes.endZ) {
            roadNetwork.addEdge(tempX,
                                tempZ,
                                tempX + bridgeAttributes.xDelta,
                                tempZ + bridgeAttributes.zDelta,
                                bridgeAttributes.roadDeckHeight,
                                CityTour.RoadNetwork.BRIDGE_SURFACE);
            tempX += bridgeAttributes.xDelta;
            tempZ += bridgeAttributes.zDelta;
          }

          branchFromIntersection(terrain, roadNetwork, bridgeAttributes.endX, bridgeAttributes.endZ);
        }
      }
      else {
        if (shouldConnectIntersections(terrain, mapX, mapZ, targetMapX, targetMapZ)) {
          if (roadNetwork.hasIntersection(targetMapX, targetMapZ)) {
            roadNetwork.addEdge(mapX, mapZ, targetMapX, targetMapZ, 0.0, CityTour.RoadNetwork.TERRAIN_SURFACE);
          }
          else {
            roadNetwork.addEdge(mapX, mapZ, targetMapX, targetMapZ, 0.0, CityTour.RoadNetwork.TERRAIN_SURFACE);
            branchFromIntersection(terrain, roadNetwork, targetMapX, targetMapZ);
          }
        }
      }
    };

    var roadNetwork = new CityTour.RoadNetwork(terrain);
    branchFromIntersection(terrain, roadNetwork, centerMapX, centerMapZ);

    return roadNetwork;
  };

  var roadNetworkGenerator = {};

  roadNetworkGenerator.generate = function(terrain, config) {
    return buildRoadNetwork(terrain, config);
  };

  return roadNetworkGenerator;
})();
<|endoftext|>