<commit_before>(function () {
    class Command {
        constructor(name, locus, orux, symbol, message) {
            this.name = name;
            this.locus = locus;
            this.orux = orux;
            this.symbol = symbol;
            this.message = message;
        }
    }

    class RoundaboutCommand extends Command {
        constructor(command, exitNumber) {
            this.name = command.name + exitNumber;
            this.locus = command.locus + exitNumber;
            this.orux = command.orux + exitNumber;
            this.symbol = command.symbol + exitNumber;
            this.message = command.message + exitNumber;
        }
    }

    class RoundaboutLeftCommand extends RoundaboutCommand {
        constructor(command, exitNumber) {
            this.name = command.name + -exitNumber;
            this.locus = command.locus + -exitNumber;
            this.orux = command.orux + exitNumber;
            this.symbol = command.symbol + -exitNumber;
            this.message = command.message + -exitNumber;
        }
    }

    BR.VoiceHints = L.Class.extend({
        statics: {
            commands: (function () {
                return {
                    1: new Command('C', 1, 1002, 'Straight', 'straight'),
                    2: new Command('TL', 4, 1000, 'Left', 'left'),
                    3: new Command('TSLL', 3, 1017, 'TSLL', 'slight left'),
                    4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp left'),
                    5: new Command('TR', 7, 1001, 'Right', 'right'),
                    6: new Command('TSLR', 6, 1016, 'TSLR', 'slight right'),
                    7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp right'),
                    8: new Command('KL', 9, 1015, 'TSLL', 'keep left'),
                    9: new Command('KR', 10, 1014, 'TSLR', 'keep right'),
                    10: new Command('TU', 13, 1003, 'TU', 'u-turn'),
                    11: new Command('TRU', 14, 1003, 'TU', 'u-turn'), // Right U-turn
                    12: new Command('OFFR'), // Off route
                    13: new Command('RNDB', 26, 1008, 'RNDB', 'Take exit '), // Roundabout
                    14: new Command('RNLB', 26, 1008, 'RNLB', 'Take exit '), // Roundabout left
                };
            })(),
        },

        initialize: function (geoJson, turnInstructionMode, transportMode) {
            this.geoJson = geoJson;
            this.turnInstructionMode = turnInstructionMode;
            this.transportMode = transportMode;

            for (const feature of geoJson.features) {
                let voicehints = feature?.properties.voicehints;
                if (voicehints) {
                    this.voicehints = voicehints;
                    this.track = feature;
                    break;
                }
            }
        },

        getGpxTransform: function () {
            const transform = {
                comment: '',
                trk: function (trk, feature, coordsList) {
                    const properties = this._getTrk();

                    return Object.assign(
                        {
                            name: feature.properties.name,
                        },
                        properties,
                        trk
                    );
                }.bind(this),
            };

            this._addToTransform(transform);

            return transform;
        },

        _getDuration: function (voicehintsIndex) {
            const timeList = this.track.properties.times;
            const indexInTrack = this.voicehints[voicehintsIndex][0];
            const currTime = timeList[indexInTrack];
            const len = this.voicehints.length;
            const nextIndex = voicehintsIndex < len - 1 ? this.voicehints[voicehintsIndex + 1][0] : timeList.length - 1;
            const nextTime = timeList[nextIndex];

            const duration = nextTime - currTime;

            // TODO remove
            const time = this.voicehints[voicehintsIndex][4];
            const p = 5;
            if (!(time.toPrecision(p) === duration.toPrecision(p))) {
                console.error(
                    `${voicehintsIndex}: ${time.toPrecision(p)} =? ${duration.toPrecision(p)}, ${time} =? ${duration}`
                );
            }

            return duration;
        },

        _loopHints: function (hintCallback) {
            for (const [i, values] of this.voicehints.entries()) {
                const [indexInTrack, commandId, exitNumber, distance, time, angle, geometry] = values;
                const hint = { indexInTrack, commandId, exitNumber, distance, time, angle, geometry };

                // TODO remove server hint time
                //hint.time = this._getDuration(i);
                this._getDuration(i);
                if (hint.time > 0) {
                    hint.speed = distance / hint.time;
                }

                const coord = this.track.geometry.coordinates[indexInTrack];
                const cmd = this.getCommand(commandId, exitNumber);
                if (!cmd) {
                    console.error(`no voicehint command for id: ${commandId} (${values})`);
                    continue;
                }

                hintCallback(hint, cmd, coord);
            }
        },

        getCommand: function (id, exitNumber) {
            let command = BR.VoiceHints.commands[id];
            if (id === 13) {
                command = new RoundaboutCommand(command, exitNumber);
            } else if (id === 14) {
                command = new RoundaboutLeftCommand(command, exitNumber);
            }
            return command;
        },

        // override in subclass
        _addToTransform: function (transform) {},

        // override in subclass
        _getTrk: function () {
            return {};
        },
    });

    BR.WaypointVoiceHints = BR.VoiceHints.extend({
        _addToTransform: function (transform) {
            transform.gpx = function (gpx, features) {
                this._addWaypoints(gpx);
                return gpx;
            }.bind(this);
        },

        _addWaypoints: function (gpx) {
            this._loopHints((hint, cmd, coord) => {
                const properties = this._getWpt(hint, cmd, coord);

                const wpt = this._createWpt(coord, properties);
                gpx.wpt.push(wpt);
            });
        },

        _createWpt: function (coord, properties) {
            return Object.assign(
                {
                    '@lat': coord[1],
                    '@lon': coord[0],
                },
                properties
            );
        },

        // override in subclass
        _getWpt: function (hint, cmd, coord) {
            return {};
        },
    });

    BR.GpsiesVoiceHints = BR.WaypointVoiceHints.extend({
        _getWpt: function (hint, cmd, coord) {
            return { name: cmd.message, sym: cmd.symbol.toLowerCase(), type: cmd.symbol };
        },
    });

    BR.OruxVoiceHints = BR.WaypointVoiceHints.extend({
        _getWpt: function (hint, cmd, coord) {
            const wpt = {
                ele: coord[2],
                extensions: {
                    'om:oruxmapsextensions': {
                        '@xmlns:om': 'http://www.oruxmaps.com/oruxmapsextensions/1/0',
                        'om:ext': { '@type': 'ICON', '@subtype': 0, _: cmd.orux },
                    },
                },
            };

            if (wpt.ele === undefined || wpt.ele === null) {
                delete wpt.ele;
            }

            return wpt;
        },
    });

    BR.LocusVoiceHints = BR.WaypointVoiceHints.extend({
        _addToTransform: function (transform) {
            transform.gpx = function (gpx, features) {
                // hack to insert attribute after the other `xmlns`s
                gpx = Object.assign(
                    {
                        '@xmlns': gpx['@xmlns'],
                        '@xmlns:xsi': gpx['@xmlns:xsi'],
                        '@xmlns:locus': 'http://www.locusmap.eu',
                    },
                    gpx
                );

                this._addWaypoints(gpx);

                return gpx;
            }.bind(this);
        },

        _getWpt: function (hint, cmd, coord) {
            const extensions = {};
            // TODO 'locus:' namespace gets removed
            extensions['locus:rteDistance'] = hint.distance;
            if (hint.time > 0) {
                extensions['locus:rteTime'] = hint.time;
                extensions['locus:rteSpeed'] = hint.speed;
            }
            extensions['locus:rtePointAction'] = cmd.locus;

            const wpt = {
                ele: coord[2],
                name: cmd.message,
                extensions: extensions,
            };

            if (wpt.ele === undefined || wpt.ele === null) {
                delete wpt.ele;
            }

            return wpt;
        },

        _getTrk: function () {
            return {
                extensions: {
                    'locus:rteComputeType': this._getLocusRouteType(this.transportMode),
                    'locus:rteSimpleRoundabouts': 1,
                },
            };
        },

        _getLocusRouteType: function (transportMode) {
            switch (transportMode) {
                case 'car':
                    return 0;
                case 'bike':
                    return 5;
                default:
                    return 3; // foot
            }
        },
    });

    BR.CommentVoiceHints = BR.VoiceHints.extend({
        _addToTransform: function (transform) {
            let comment = `
<!-- $transport-mode$${this.transportMode}$ -->
<!--          cmd    idx        lon        lat d2next  geometry -->
<!-- $turn-instruction-start$
`;

            this._loopHints((hint, cmd, coord) => {
                const pad = (obj = '', len) => {
                    return new String(obj).padStart(len) + ';';
                };

                let turn = '';
                turn += pad(cmd.name, 6);
                turn += pad(hint.indexInTrack, 6);
                turn += pad(coord[0], 10);
                turn += pad(coord[1], 10);
                turn += pad(hint.distance, 6);
                turn += hint.geometry;

                comment += `
     $turn$${turn}$
`;
            });

            comment += `
    $turn-instruction-end$ -->
`;

            transform.comment = comment;
        },
    });

    BR.OsmAndVoiceHints = BR.VoiceHints.extend({
        _addToTransform: function (transform) {
            transform.gpx = function (gpx, features) {
                gpx['@creator'] = 'OsmAndRouter';

                gpx.rte.push({
                    rtept: this._createRoutePoints(gpx),
                });

                // reorder trk after rte
                const trk = gpx.trk;
                delete gpx.trk;
                gpx.trk = trk;

                return gpx;
            }.bind(this);
        },

        _createRoutePoints: function (gpx) {
            const rteptList = [];

            const trkseg = gpx.trk[0].trkseg[0];
            let trkpt = trkseg.trkpt[0];
            const startTime = this.track.properties.times[this.voicehints[0][0]];
            rteptList.push({
                '@lat': trkpt['@lat'],
                '@lon': trkpt['@lon'],
                desc: 'start',
                extensions: { time: Math.round(startTime), offset: 0 },
            });

            this._loopHints((hint, cmd, coord) => {
                const rtept = {
                    '@lat': coord[1],
                    '@lon': coord[0],
                    desc: cmd.message,
                    extensions: {
                        time: Math.round(hint.time),
                        turn: cmd.name,
                        'turn-angle': hint.angle,
                        offset: hint.indexInTrack,
                    },
                };

                if (hint.time === 0) {
                    delete properties.extensions.time;
                }

                rteptList.push(rtept);
            });

            const lastIndex = trkseg.trkpt.length - 1;
            trkpt = trkseg.trkpt[lastIndex];
            rteptList.push({
                '@lat': trkpt['@lat'],
                '@lon': trkpt['@lon'],
                desc: 'destination',
                extensions: { time: 0, offset: lastIndex },
            });

            return rteptList;
        },
    });

    BR.voiceHints = function (geoJson, turnInstructionMode, transportMode) {
        switch (turnInstructionMode) {
            case 2:
                return new BR.LocusVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 3:
                return new BR.OsmAndVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 4:
                return new BR.CommentVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 5:
                return new BR.GpsiesVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 6:
                return new BR.OruxVoiceHints(geoJson, turnInstructionMode, transportMode);
            default:
                console.error('unhandled turnInstructionMode: ' + turnInstructionMode);
                return new BR.VoiceHints(geoJson, turnInstructionMode, transportMode);
        }
    };
})();
<commit_msg>Handle missing voicehints and times<commit_after>(function () {
    class Command {
        constructor(name, locus, orux, symbol, message) {
            this.name = name;
            this.locus = locus;
            this.orux = orux;
            this.symbol = symbol;
            this.message = message;
        }
    }

    class RoundaboutCommand extends Command {
        constructor(command, exitNumber) {
            this.name = command.name + exitNumber;
            this.locus = command.locus + exitNumber;
            this.orux = command.orux + exitNumber;
            this.symbol = command.symbol + exitNumber;
            this.message = command.message + exitNumber;
        }
    }

    class RoundaboutLeftCommand extends RoundaboutCommand {
        constructor(command, exitNumber) {
            this.name = command.name + -exitNumber;
            this.locus = command.locus + -exitNumber;
            this.orux = command.orux + exitNumber;
            this.symbol = command.symbol + -exitNumber;
            this.message = command.message + -exitNumber;
        }
    }

    BR.VoiceHints = L.Class.extend({
        statics: {
            commands: (function () {
                return {
                    1: new Command('C', 1, 1002, 'Straight', 'straight'),
                    2: new Command('TL', 4, 1000, 'Left', 'left'),
                    3: new Command('TSLL', 3, 1017, 'TSLL', 'slight left'),
                    4: new Command('TSHL', 5, 1019, 'TSHL', 'sharp left'),
                    5: new Command('TR', 7, 1001, 'Right', 'right'),
                    6: new Command('TSLR', 6, 1016, 'TSLR', 'slight right'),
                    7: new Command('TSHR', 8, 1018, 'TSHR', 'sharp right'),
                    8: new Command('KL', 9, 1015, 'TSLL', 'keep left'),
                    9: new Command('KR', 10, 1014, 'TSLR', 'keep right'),
                    10: new Command('TU', 13, 1003, 'TU', 'u-turn'),
                    11: new Command('TRU', 14, 1003, 'TU', 'u-turn'), // Right U-turn
                    12: new Command('OFFR'), // Off route
                    13: new Command('RNDB', 26, 1008, 'RNDB', 'Take exit '), // Roundabout
                    14: new Command('RNLB', 26, 1008, 'RNLB', 'Take exit '), // Roundabout left
                };
            })(),
        },

        initialize: function (geoJson, turnInstructionMode, transportMode) {
            this.geoJson = geoJson;
            this.turnInstructionMode = turnInstructionMode;
            this.transportMode = transportMode;

            this.track = geoJson.features?.[0];
            this.voicehints = this.track?.properties?.voicehints;
        },

        getGpxTransform: function () {
            const transform = {
                comment: '',
                trk: function (trk, feature, coordsList) {
                    const properties = this._getTrk();

                    return Object.assign(
                        {
                            name: feature.properties.name,
                        },
                        properties,
                        trk
                    );
                }.bind(this),
            };

            this._addToTransform(transform);

            return transform;
        },

        _getDuration: function (voicehintsIndex) {
            const timeList = this.track.properties.times;
            const indexInTrack = this.voicehints[voicehintsIndex][0];
            const currTime = timeList[indexInTrack];
            const len = this.voicehints.length;
            const nextIndex = voicehintsIndex < len - 1 ? this.voicehints[voicehintsIndex + 1][0] : timeList.length - 1;
            const nextTime = timeList[nextIndex];

            const duration = nextTime - currTime;

            // TODO remove
            const time = this.voicehints[voicehintsIndex][4];
            const p = 5;
            if (!(time.toPrecision(p) === duration.toPrecision(p))) {
                console.error(
                    `${voicehintsIndex}: ${time.toPrecision(p)} =? ${duration.toPrecision(p)}, ${time} =? ${duration}`
                );
            }

            return duration;
        },

        _loopHints: function (hintCallback) {
            if (!this.voicehints) return;
            for (const [i, values] of this.voicehints.entries()) {
                const [indexInTrack, commandId, exitNumber, distance, time, angle, geometry] = values;
                const hint = { indexInTrack, commandId, exitNumber, distance, time, angle, geometry };

                // TODO remove server hint time
                //hint.time = this._getDuration(i);
                this._getDuration(i);
                if (hint.time > 0) {
                    hint.speed = distance / hint.time;
                }

                const coord = this.track.geometry.coordinates[indexInTrack];
                const cmd = this.getCommand(commandId, exitNumber);
                if (!cmd) {
                    console.error(`no voicehint command for id: ${commandId} (${values})`);
                    continue;
                }

                hintCallback(hint, cmd, coord);
            }
        },

        getCommand: function (id, exitNumber) {
            let command = BR.VoiceHints.commands[id];
            if (id === 13) {
                command = new RoundaboutCommand(command, exitNumber);
            } else if (id === 14) {
                command = new RoundaboutLeftCommand(command, exitNumber);
            }
            return command;
        },

        // override in subclass
        _addToTransform: function (transform) {},

        // override in subclass
        _getTrk: function () {
            return {};
        },
    });

    BR.WaypointVoiceHints = BR.VoiceHints.extend({
        _addToTransform: function (transform) {
            transform.gpx = function (gpx, features) {
                this._addWaypoints(gpx);
                return gpx;
            }.bind(this);
        },

        _addWaypoints: function (gpx) {
            this._loopHints((hint, cmd, coord) => {
                const properties = this._getWpt(hint, cmd, coord);

                const wpt = this._createWpt(coord, properties);
                gpx.wpt.push(wpt);
            });
        },

        _createWpt: function (coord, properties) {
            return Object.assign(
                {
                    '@lat': coord[1],
                    '@lon': coord[0],
                },
                properties
            );
        },

        // override in subclass
        _getWpt: function (hint, cmd, coord) {
            return {};
        },
    });

    BR.GpsiesVoiceHints = BR.WaypointVoiceHints.extend({
        _getWpt: function (hint, cmd, coord) {
            return { name: cmd.message, sym: cmd.symbol.toLowerCase(), type: cmd.symbol };
        },
    });

    BR.OruxVoiceHints = BR.WaypointVoiceHints.extend({
        _getWpt: function (hint, cmd, coord) {
            const wpt = {
                ele: coord[2],
                extensions: {
                    'om:oruxmapsextensions': {
                        '@xmlns:om': 'http://www.oruxmaps.com/oruxmapsextensions/1/0',
                        'om:ext': { '@type': 'ICON', '@subtype': 0, _: cmd.orux },
                    },
                },
            };

            if (wpt.ele === undefined || wpt.ele === null) {
                delete wpt.ele;
            }

            return wpt;
        },
    });

    BR.LocusVoiceHints = BR.WaypointVoiceHints.extend({
        _addToTransform: function (transform) {
            transform.gpx = function (gpx, features) {
                // hack to insert attribute after the other `xmlns`s
                gpx = Object.assign(
                    {
                        '@xmlns': gpx['@xmlns'],
                        '@xmlns:xsi': gpx['@xmlns:xsi'],
                        '@xmlns:locus': 'http://www.locusmap.eu',
                    },
                    gpx
                );

                this._addWaypoints(gpx);

                return gpx;
            }.bind(this);
        },

        _getWpt: function (hint, cmd, coord) {
            const extensions = {};
            // TODO 'locus:' namespace gets removed
            extensions['locus:rteDistance'] = hint.distance;
            if (hint.time > 0) {
                extensions['locus:rteTime'] = hint.time;
                extensions['locus:rteSpeed'] = hint.speed;
            }
            extensions['locus:rtePointAction'] = cmd.locus;

            const wpt = {
                ele: coord[2],
                name: cmd.message,
                extensions: extensions,
            };

            if (wpt.ele === undefined || wpt.ele === null) {
                delete wpt.ele;
            }

            return wpt;
        },

        _getTrk: function () {
            return {
                extensions: {
                    'locus:rteComputeType': this._getLocusRouteType(this.transportMode),
                    'locus:rteSimpleRoundabouts': 1,
                },
            };
        },

        _getLocusRouteType: function (transportMode) {
            switch (transportMode) {
                case 'car':
                    return 0;
                case 'bike':
                    return 5;
                default:
                    return 3; // foot
            }
        },
    });

    BR.CommentVoiceHints = BR.VoiceHints.extend({
        _addToTransform: function (transform) {
            let comment = `
<!-- $transport-mode$${this.transportMode}$ -->
<!--          cmd    idx        lon        lat d2next  geometry -->
<!-- $turn-instruction-start$
`;

            this._loopHints((hint, cmd, coord) => {
                const pad = (obj = '', len) => {
                    return new String(obj).padStart(len) + ';';
                };

                let turn = '';
                turn += pad(cmd.name, 6);
                turn += pad(hint.indexInTrack, 6);
                turn += pad(coord[0], 10);
                turn += pad(coord[1], 10);
                turn += pad(hint.distance, 6);
                turn += hint.geometry;

                comment += `
     $turn$${turn}$
`;
            });

            comment += `
    $turn-instruction-end$ -->
`;

            transform.comment = comment;
        },
    });

    BR.OsmAndVoiceHints = BR.VoiceHints.extend({
        _addToTransform: function (transform) {
            transform.gpx = function (gpx, features) {
                gpx['@creator'] = 'OsmAndRouter';

                gpx.rte.push({
                    rtept: this._createRoutePoints(gpx),
                });

                // reorder trk after rte
                const trk = gpx.trk;
                delete gpx.trk;
                gpx.trk = trk;

                return gpx;
            }.bind(this);
        },

        _createRoutePoints: function (gpx) {
            const rteptList = [];

            const trkseg = gpx.trk[0].trkseg[0];
            let trkpt = trkseg.trkpt[0];
            const startPt = {
                '@lat': trkpt['@lat'],
                '@lon': trkpt['@lon'],
                desc: 'start',
            };
            const times = this.track?.properties?.times;
            if (this.voicehints && times) {
                const startTime = times[this.voicehints[0][0]];
                startPt.extensions = { time: Math.round(startTime), offset: 0 };
            }
            rteptList.push(startPt);

            this._loopHints((hint, cmd, coord) => {
                const rtept = {
                    '@lat': coord[1],
                    '@lon': coord[0],
                    desc: cmd.message,
                    extensions: {
                        time: Math.round(hint.time),
                        turn: cmd.name,
                        'turn-angle': hint.angle,
                        offset: hint.indexInTrack,
                    },
                };

                if (hint.time === 0) {
                    delete properties.extensions.time;
                }

                rteptList.push(rtept);
            });

            const lastIndex = trkseg.trkpt.length - 1;
            trkpt = trkseg.trkpt[lastIndex];
            rteptList.push({
                '@lat': trkpt['@lat'],
                '@lon': trkpt['@lon'],
                desc: 'destination',
                extensions: { time: 0, offset: lastIndex },
            });

            return rteptList;
        },
    });

    BR.voiceHints = function (geoJson, turnInstructionMode, transportMode) {
        switch (turnInstructionMode) {
            case 2:
                return new BR.LocusVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 3:
                return new BR.OsmAndVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 4:
                return new BR.CommentVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 5:
                return new BR.GpsiesVoiceHints(geoJson, turnInstructionMode, transportMode);
            case 6:
                return new BR.OruxVoiceHints(geoJson, turnInstructionMode, transportMode);
            default:
                console.error('unhandled turnInstructionMode: ' + turnInstructionMode);
                return new BR.VoiceHints(geoJson, turnInstructionMode, transportMode);
        }
    };
})();
<|endoftext|>


<commit_before>/**
 * Reveal module.
 * @module foundation.reveal
 * @requires foundation.util.keyboard
 * @requires foundation.util.box
 * @requires foundation.util.triggers
 * @requires foundation.util.mediaQuery
 * @requires foundation.util.motion if using animations
 */
!function(Foundation, $) {
  'use strict';

  /**
   * Creates a new instance of Reveal.
   * @class
   * @param {jQuery} element - jQuery object to use for the modal.
   * @param {Object} options - optional parameters.
   */

  function Reveal(element, options) {
    this.$element = element;
    this.options = $.extend({}, Reveal.defaults, this.$element.data(), options);
    this._init();

    Foundation.registerPlugin(this);
    Foundation.Keyboard.register('Reveal', {
      'ENTER': 'open',
      'SPACE': 'open',
      'ESCAPE': 'close',
      'TAB': 'tab_forward',
      'SHIFT_TAB': 'tab_backward'
    });
  }

  Reveal.defaults = {
    /**
     * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
     * @option
     * @example 'slide-in-left'
     */
    animationIn: '',
    /**
     * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
     * @option
     * @example 'slide-out-right'
     */
    animationOut: '',
    /**
     * Time, in ms, to delay the opening of a modal after a click if no animation used.
     * @option
     * @example 10
     */
    showDelay: 0,
    /**
     * Time, in ms, to delay the closing of a modal after a click if no animation used.
     * @option
     * @example 10
     */
    hideDelay: 0,
    /**
     * Allows a click on the body/overlay to close the modal.
     * @option
     * @example true
     */
    closeOnClick: true,
    /**
     * Allows the modal to close if the user presses the `ESCAPE` key.
     * @option
     * @example true
     */
    closeOnEsc: true,
    /**
     * If true, allows multiple modals to be displayed at once.
     * @option
     * @example false
     */
    multipleOpened: false,
    /**
     * Distance, in pixels, the modal should push down from the top of the screen.
     * @option
     * @example 100
     */
    vOffset: 100,
    /**
     * Distance, in pixels, the modal should push in from the side of the screen.
     * @option
     * @example 0
     */
    hOffset: 0,
    /**
     * Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well.
     * @option
     * @example false
     */
    fullScreen: false,
    /**
     * Percentage of screen height the modal should push up from the bottom of the view.
     * @option
     * @example 10
     */
    btmOffsetPct: 10,
    /**
     * Allows the modal to generate an overlay div, which will cover the view when modal opens.
     * @option
     * @example true
     */
    overlay: true,
    /**
     * Allows the modal to remove and reinject markup on close. Should be true if using video elements w/o using provider's api.
     * @option
     * @example false
     */
    resetOnClose: false
  };

  /**
   * Initializes the modal by adding the overlay and close buttons, (if selected).
   * @private
   */
  Reveal.prototype._init = function(){
    this.id = this.$element.attr('id');
    this.isActive = false;

    this.$anchor = $('[data-open="' + this.id + '"]').length ? $('[data-open="' + this.id + '"]') : $('[data-toggle="' + this.id + '"]');

    if(this.$anchor.length){
      var anchorId = this.$anchor[0].id || Foundation.GetYoDigits(6, 'reveal');

      this.$anchor.attr({
        'aria-controls': this.id,
        'id': anchorId,
        'aria-haspopup': true,
        'tabindex': 0
      });
      this.$element.attr({'aria-labelledby': anchorId});
    }

    // this.options.fullScreen = this.$element.hasClass('full');
    if(this.options.fullScreen || this.$element.hasClass('full')){
      this.options.fullScreen = true;
      this.options.overlay = false;
    }
    if(this.options.overlay && !this.$overlay){
      this.$overlay = this._makeOverlay(this.id);
    }

    this.$element.attr({
        'role': 'dialog',
        'aria-hidden': true,
        'data-yeti-box': this.id,
        'data-resize': this.id
    });

    this._events();
  };

  /**
   * Creates an overlay div to display behind the modal.
   * @private
   */
  Reveal.prototype._makeOverlay = function(id){
    var $overlay = $('

')
                    .addClass('reveal-overlay')
                    .attr({'tabindex': -1, 'aria-hidden': true})
                    .appendTo('body');
    if(this.options.closeOnClick){
      $overlay.attr({
        'data-close': id
      });
    }
    return $overlay;
  };

  /**
   * Adds event handlers for the modal.
   * @private
   */
  Reveal.prototype._events = function(){
    var _this = this;

    this.$element.on({
      'open.zf.trigger': this.open.bind(this),
      'close.zf.trigger': this.close.bind(this),
      'toggle.zf.trigger': this.toggle.bind(this),
      'resizeme.zf.trigger': function(){
        if(_this.$element.is(':visible')){
          _this._setPosition(function(){});
        }
      }
    });

    if(this.$anchor.length){
      this.$anchor.on('keydown.zf.reveal', function(e){
        if(e.which === 13 || e.which === 32){
          e.stopPropagation();
          e.preventDefault();
          _this.open();
        }
      });
    }

    if(this.options.closeOnClick && this.options.overlay){
      this.$overlay.off('.zf.reveal').on('click.zf.reveal', this.close.bind(this));
    }
  };
  /**
   * Sets the position of the modal before opening
   * @param {Function} cb - a callback function to execute when positioning is complete.
   * @private
   */
  Reveal.prototype._setPosition = function(cb){
    var eleDims = Foundation.Box.GetDimensions(this.$element);
    var elePos = this.options.fullScreen ? 'reveal full' : (eleDims.height >= (0.5 * eleDims.windowDims.height)) ? 'reveal' : 'center';

    if(elePos === 'reveal full'){
      //set to full height/width
      this.$element
          .offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset))
          .css({
            'height': eleDims.windowDims.height,
            'width': eleDims.windowDims.width
          });
    }else if(!Foundation.MediaQuery.atLeast('medium') || !Foundation.Box.ImNotTouchingYou(this.$element, null, true, false)){
      //if smaller than medium, resize to 100% width minus any custom L/R margin
      this.$element
          .css({
            'width': eleDims.windowDims.width - (this.options.hOffset * 2)
          })
          .offset(Foundation.Box.GetOffsets(this.$element, null, 'center', this.options.vOffset, this.options.hOffset));
      //flag a boolean so we can reset the size after the element is closed.
      this.changedSize = true;
    }else{
      this.$element
          .css({
            'max-height': eleDims.windowDims.height - (this.options.vOffset * (this.options.btmOffsetPct / 100 + 1)),
            'width': ''
          })
          .offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset));
          //the max height based on a percentage of vertical offset plus vertical offset
    }

    cb();
  };

  /**
   * Opens the modal controlled by `this.$anchor`, and closes all others by default.
   * @function
   * @fires Reveal#closeAll
   * @fires Reveal#open
   */
  Reveal.prototype.open = function(){
    var _this = this;
    this.isActive = true;
    //make element invisible, but remove display: none so we can get size and positioning
    this.$element
        .css({'visibility': 'hidden'})
        .show()
        .scrollTop(0);

    this._setPosition(function(){
      _this.$element.hide()
                   .css({'visibility': ''});
      if(!_this.options.multipleOpened){
        /**
         * Fires immediately before the modal opens.
         * Closes any other modals that are currently open
         * @event Reveal#closeAll
         */
        _this.$element.trigger('closeme.zf.reveal', _this.id);
      }
      if(_this.options.animationIn){
        if(_this.options.overlay){
          Foundation.Motion.animateIn(_this.$overlay, 'fade-in', function(){
            Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){
              _this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element);
            });
          });
        }else{
          Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){
            _this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element);
          });
        }
      }else{
        if(_this.options.overlay){
          _this.$overlay.show(0, function(){
            _this.$element.show(_this.options.showDelay, function(){
            });
          });
        }else{
          _this.$element.show(_this.options.showDelay, function(){
          });
        }
      }
    });

    // handle accessibility
    this.$element.attr({'aria-hidden': false}).attr('tabindex', -1).focus()
    /**
     * Fires when the modal has successfully opened.
     * @event Reveal#open
     */
                 .trigger('open.zf.reveal');

    $('body').addClass('is-reveal-open')
             .attr({'aria-hidden': (this.options.overlay || this.options.fullScreen) ? true : false});
    setTimeout(function(){
      _this._extraHandlers();
      // Foundation.reflow();
    }, 0);
  };

  /**
   * Adds extra event handlers for the body and window if necessary.
   * @private
   */
  Reveal.prototype._extraHandlers = function(){
    var _this = this;
    this.focusableElements = Foundation.Keyboard.findFocusable(this.$element);

    if(!this.options.overlay && this.options.closeOnClick && !this.options.fullScreen){
      $('body').on('click.zf.reveal', function(e){
        // if()
          _this.close();
      });
    }
    if(this.options.closeOnEsc){
      $(window).on('keydown.zf.reveal', function(e){
        Foundation.Keyboard.handleKey(e, 'Reveal', {
          close: function() {
            if (_this.options.closeOnEsc) {
              _this.close();
              _this.$anchor.focus();
            }
          }
        });
        if (_this.focusableElements.length === 0) { // no focusable elements inside the modal at all, prevent tabbing in general
          e.preventDefault();
        }
      });
    }

    // lock focus within modal while tabbing
    this.$element.on('keydown.zf.reveal', function(e) {
      var $target = $(this);
      // handle keyboard event with keyboard util
      Foundation.Keyboard.handleKey(e, 'Reveal', {
        tab_forward: function() {
          if (_this.$element.find(':focus').is(_this.focusableElements.eq(-1))) { // left modal downwards, setting focus to first element
            _this.focusableElements.eq(0).focus();
            e.preventDefault();
          }
        },
        tab_backward: function() {
          if (_this.$element.find(':focus').is(_this.focusableElements.eq(0)) || _this.$element.is(':focus')) { // left modal upwards, setting focus to last element
            _this.focusableElements.eq(-1).focus();
            e.preventDefault();
          }
        },
        open: function() {
          if (_this.$element.find(':focus').is(_this.$element.find('[data-close]'))) {
            setTimeout(function() { // set focus back to anchor if close button has been activated
              _this.$anchor.focus();
            }, 1);
          } else if ($target.is(_this.focusableElements)) { // dont't trigger if acual element has focus (i.e. inputs, links, ...)
            _this.open();
          }
        },
        close: function() {
          if (_this.options.closeOnEsc) {
            _this.close();
            _this.$anchor.focus();
          }
        }
      });
    });

  };

  /**
   * Closes the modal.
   * @function
   * @fires Reveal#closed
   */
  Reveal.prototype.close = function(){
    if(!this.isActive || !this.$element.is(':visible')){
      return false;
    }
    var _this = this;

    if(this.options.animationOut){
      Foundation.Motion.animateOut(this.$element, this.options.animationOut, function(){
        if(_this.options.overlay){
          Foundation.Motion.animateOut(_this.$overlay, 'fade-out', function(){
          });
        }
      });
    }else{
      this.$element.hide(_this.options.hideDelay, function(){
        if(_this.options.overlay){
          _this.$overlay.hide(0, function(){
          });
        }
      });
    }
    //conditionals to remove extra event listeners added on open
    if(this.options.closeOnEsc){
      $(window).off('keydown.zf.reveal');
    }
    if(!this.options.overlay && this.options.closeOnClick){
      $('body').off('click.zf.reveal');
    }
    this.$element.off('keydown.zf.reveal');

    //if the modal changed size, reset it
    if(this.changedSize){
      this.$element.css({
        'height': '',
        'width': ''
      });
    }

    $('body').removeClass('is-reveal-open').attr({'aria-hidden': false, 'tabindex': ''});

    /**
    * Resets the modal content
    * This prevents a running video to keep going in the background
    */
    if(this.options.resetOnClose) {
      this.$element.html(this.$element.html());
    }

    this.isActive = false;
    this.$element.attr({'aria-hidden': true})
    /**
     * Fires when the modal is done closing.
     * @event Reveal#closed
     */
                 .trigger('closed.zf.reveal');
  };
  /**
   * Toggles the open/closed state of a modal.
   * @function
   */
  Reveal.prototype.toggle = function(){
    if(this.isActive){
      this.close();
    }else{
      this.open();
    }
  };

  /**
   * Destroys an instance of a modal.
   * @function
   */
  Reveal.prototype.destroy = function() {
    if(this.options.overlay){
      this.$overlay.hide().off().remove();
    }
    this.$element.hide();
    this.$anchor.off();

    Foundation.unregisterPlugin(this);
  };

  Foundation.plugin(Reveal, 'Reveal');

  // Exports for AMD/Browserify
  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
    module.exports = Reveal;
  if (typeof define === 'function')
    define(['foundation'], function() {
      return Reveal;
    });

}(Foundation, jQuery);
<commit_msg>fixes #7589<commit_after>/**
 * Reveal module.
 * @module foundation.reveal
 * @requires foundation.util.keyboard
 * @requires foundation.util.box
 * @requires foundation.util.triggers
 * @requires foundation.util.mediaQuery
 * @requires foundation.util.motion if using animations
 */
!function(Foundation, $) {
  'use strict';

  /**
   * Creates a new instance of Reveal.
   * @class
   * @param {jQuery} element - jQuery object to use for the modal.
   * @param {Object} options - optional parameters.
   */

  function Reveal(element, options) {
    this.$element = element;
    this.options = $.extend({}, Reveal.defaults, this.$element.data(), options);
    this._init();

    Foundation.registerPlugin(this);
    Foundation.Keyboard.register('Reveal', {
      'ENTER': 'open',
      'SPACE': 'open',
      'ESCAPE': 'close',
      'TAB': 'tab_forward',
      'SHIFT_TAB': 'tab_backward'
    });
  }

  Reveal.defaults = {
    /**
     * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
     * @option
     * @example 'slide-in-left'
     */
    animationIn: '',
    /**
     * Motion-UI class to use for animated elements. If none used, defaults to simple show/hide.
     * @option
     * @example 'slide-out-right'
     */
    animationOut: '',
    /**
     * Time, in ms, to delay the opening of a modal after a click if no animation used.
     * @option
     * @example 10
     */
    showDelay: 0,
    /**
     * Time, in ms, to delay the closing of a modal after a click if no animation used.
     * @option
     * @example 10
     */
    hideDelay: 0,
    /**
     * Allows a click on the body/overlay to close the modal.
     * @option
     * @example true
     */
    closeOnClick: true,
    /**
     * Allows the modal to close if the user presses the `ESCAPE` key.
     * @option
     * @example true
     */
    closeOnEsc: true,
    /**
     * If true, allows multiple modals to be displayed at once.
     * @option
     * @example false
     */
    multipleOpened: false,
    /**
     * Distance, in pixels, the modal should push down from the top of the screen.
     * @option
     * @example 100
     */
    vOffset: 100,
    /**
     * Distance, in pixels, the modal should push in from the side of the screen.
     * @option
     * @example 0
     */
    hOffset: 0,
    /**
     * Allows the modal to be fullscreen, completely blocking out the rest of the view. JS checks for this as well.
     * @option
     * @example false
     */
    fullScreen: false,
    /**
     * Percentage of screen height the modal should push up from the bottom of the view.
     * @option
     * @example 10
     */
    btmOffsetPct: 10,
    /**
     * Allows the modal to generate an overlay div, which will cover the view when modal opens.
     * @option
     * @example true
     */
    overlay: true,
    /**
     * Allows the modal to remove and reinject markup on close. Should be true if using video elements w/o using provider's api.
     * @option
     * @example false
     */
    resetOnClose: false
  };

  /**
   * Initializes the modal by adding the overlay and close buttons, (if selected).
   * @private
   */
  Reveal.prototype._init = function(){
    this.id = this.$element.attr('id');
    this.isActive = false;

    this.$anchor = $('[data-open="' + this.id + '"]').length ? $('[data-open="' + this.id + '"]') : $('[data-toggle="' + this.id + '"]');

    if(this.$anchor.length){
      var anchorId = this.$anchor[0].id || Foundation.GetYoDigits(6, 'reveal');

      this.$anchor.attr({
        'aria-controls': this.id,
        'id': anchorId,
        'aria-haspopup': true,
        'tabindex': 0
      });
      this.$element.attr({'aria-labelledby': anchorId});
    }

    // this.options.fullScreen = this.$element.hasClass('full');
    if(this.options.fullScreen || this.$element.hasClass('full')){
      this.options.fullScreen = true;
      this.options.overlay = false;
    }
    if(this.options.overlay && !this.$overlay){
      this.$overlay = this._makeOverlay(this.id);
    }

    this.$element.attr({
        'role': 'dialog',
        'aria-hidden': true,
        'data-yeti-box': this.id,
        'data-resize': this.id
    });

    this._events();
  };

  /**
   * Creates an overlay div to display behind the modal.
   * @private
   */
  Reveal.prototype._makeOverlay = function(id){
    var $overlay = $('

')
                    .addClass('reveal-overlay')
                    .attr({'tabindex': -1, 'aria-hidden': true})
                    .appendTo('body');
    if(this.options.closeOnClick){
      $overlay.attr({
        'data-close': id
      });
    }
    return $overlay;
  };

  /**
   * Adds event handlers for the modal.
   * @private
   */
  Reveal.prototype._events = function(){
    var _this = this;

    this.$element.on({
      'open.zf.trigger': this.open.bind(this),
      'close.zf.trigger': this.close.bind(this),
      'toggle.zf.trigger': this.toggle.bind(this),
      'resizeme.zf.trigger': function(){
        if(_this.$element.is(':visible')){
          _this._setPosition(function(){});
        }
      }
    });

    if(this.$anchor.length){
      this.$anchor.on('keydown.zf.reveal', function(e){
        if(e.which === 13 || e.which === 32){
          e.stopPropagation();
          e.preventDefault();
          _this.open();
        }
      });
    }

    if(this.options.closeOnClick && this.options.overlay){
      this.$overlay.off('.zf.reveal').on('click.zf.reveal', this.close.bind(this));
    }
  };
  /**
   * Sets the position of the modal before opening
   * @param {Function} cb - a callback function to execute when positioning is complete.
   * @private
   */
  Reveal.prototype._setPosition = function(cb){
    var eleDims = Foundation.Box.GetDimensions(this.$element);
    var elePos = this.options.fullScreen ? 'reveal full' : (eleDims.height >= (0.5 * eleDims.windowDims.height)) ? 'reveal' : 'center';

    if(elePos === 'reveal full'){
      //set to full height/width
      this.$element
          .offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset))
          .css({
            'height': eleDims.windowDims.height,
            'width': eleDims.windowDims.width
          });
    }else if(!Foundation.MediaQuery.atLeast('medium') || !Foundation.Box.ImNotTouchingYou(this.$element, null, true, false)){
      //if smaller than medium, resize to 100% width minus any custom L/R margin
      this.$element
          .css({
            'width': eleDims.windowDims.width - (this.options.hOffset * 2)
          })
          .offset(Foundation.Box.GetOffsets(this.$element, null, 'center', this.options.vOffset, this.options.hOffset));
      //flag a boolean so we can reset the size after the element is closed.
      this.changedSize = true;
    }else{
      this.$element
          .css({
            'max-height': eleDims.windowDims.height - (this.options.vOffset * (this.options.btmOffsetPct / 100 + 1)),
            'width': ''
          })
          .offset(Foundation.Box.GetOffsets(this.$element, null, elePos, this.options.vOffset));
          //the max height based on a percentage of vertical offset plus vertical offset
    }

    cb();
  };

  /**
   * Opens the modal controlled by `this.$anchor`, and closes all others by default.
   * @function
   * @fires Reveal#closeme
   * @fires Reveal#open
   */
  Reveal.prototype.open = function(){
    var _this = this;
    this.isActive = true;
    //make element invisible, but remove display: none so we can get size and positioning
    this.$element
        .css({'visibility': 'hidden'})
        .show()
        .scrollTop(0);

    this._setPosition(function(){
      _this.$element.hide()
                   .css({'visibility': ''});
      if(!_this.options.multipleOpened){
        /**
         * Fires immediately before the modal opens.
         * Closes any other modals that are currently open
         * @event Reveal#closeme
         */
        _this.$element.trigger('closeme.zf.reveal', _this.id);
      }
      if(_this.options.animationIn){
        if(_this.options.overlay){
          Foundation.Motion.animateIn(_this.$overlay, 'fade-in', function(){
            Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){
              _this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element);
            });
          });
        }else{
          Foundation.Motion.animateIn(_this.$element, _this.options.animationIn, function(){
            _this.focusableElements = Foundation.Keyboard.findFocusable(_this.$element);
          });
        }
      }else{
        if(_this.options.overlay){
          _this.$overlay.show(0, function(){
            _this.$element.show(_this.options.showDelay, function(){
            });
          });
        }else{
          _this.$element.show(_this.options.showDelay, function(){
          });
        }
      }
    });

    // handle accessibility
    this.$element.attr({'aria-hidden': false}).attr('tabindex', -1).focus()
    /**
     * Fires when the modal has successfully opened.
     * @event Reveal#open
     */
                 .trigger('open.zf.reveal');

    $('body').addClass('is-reveal-open')
             .attr({'aria-hidden': (this.options.overlay || this.options.fullScreen) ? true : false});
    setTimeout(function(){
      _this._extraHandlers();
      // Foundation.reflow();
    }, 0);
  };

  /**
   * Adds extra event handlers for the body and window if necessary.
   * @private
   */
  Reveal.prototype._extraHandlers = function(){
    var _this = this;
    this.focusableElements = Foundation.Keyboard.findFocusable(this.$element);

    if(!this.options.overlay && this.options.closeOnClick && !this.options.fullScreen){
      $('body').on('click.zf.reveal', function(e){
        // if()
          _this.close();
      });
    }
    if(this.options.closeOnEsc){
      $(window).on('keydown.zf.reveal', function(e){
        Foundation.Keyboard.handleKey(e, 'Reveal', {
          close: function() {
            if (_this.options.closeOnEsc) {
              _this.close();
              _this.$anchor.focus();
            }
          }
        });
        if (_this.focusableElements.length === 0) { // no focusable elements inside the modal at all, prevent tabbing in general
          e.preventDefault();
        }
      });
    }

    // lock focus within modal while tabbing
    this.$element.on('keydown.zf.reveal', function(e) {
      var $target = $(this);
      // handle keyboard event with keyboard util
      Foundation.Keyboard.handleKey(e, 'Reveal', {
        tab_forward: function() {
          if (_this.$element.find(':focus').is(_this.focusableElements.eq(-1))) { // left modal downwards, setting focus to first element
            _this.focusableElements.eq(0).focus();
            e.preventDefault();
          }
        },
        tab_backward: function() {
          if (_this.$element.find(':focus').is(_this.focusableElements.eq(0)) || _this.$element.is(':focus')) { // left modal upwards, setting focus to last element
            _this.focusableElements.eq(-1).focus();
            e.preventDefault();
          }
        },
        open: function() {
          if (_this.$element.find(':focus').is(_this.$element.find('[data-close]'))) {
            setTimeout(function() { // set focus back to anchor if close button has been activated
              _this.$anchor.focus();
            }, 1);
          } else if ($target.is(_this.focusableElements)) { // dont't trigger if acual element has focus (i.e. inputs, links, ...)
            _this.open();
          }
        },
        close: function() {
          if (_this.options.closeOnEsc) {
            _this.close();
            _this.$anchor.focus();
          }
        }
      });
    });

  };

  /**
   * Closes the modal.
   * @function
   * @fires Reveal#closed
   */
  Reveal.prototype.close = function(){
    if(!this.isActive || !this.$element.is(':visible')){
      return false;
    }
    var _this = this;

    if(this.options.animationOut){
      Foundation.Motion.animateOut(this.$element, this.options.animationOut, function(){
        if(_this.options.overlay){
          Foundation.Motion.animateOut(_this.$overlay, 'fade-out', function(){
          });
        }
      });
    }else{
      this.$element.hide(_this.options.hideDelay, function(){
        if(_this.options.overlay){
          _this.$overlay.hide(0, function(){
          });
        }
      });
    }
    //conditionals to remove extra event listeners added on open
    if(this.options.closeOnEsc){
      $(window).off('keydown.zf.reveal');
    }
    if(!this.options.overlay && this.options.closeOnClick){
      $('body').off('click.zf.reveal');
    }
    this.$element.off('keydown.zf.reveal');

    //if the modal changed size, reset it
    if(this.changedSize){
      this.$element.css({
        'height': '',
        'width': ''
      });
    }

    $('body').removeClass('is-reveal-open').attr({'aria-hidden': false, 'tabindex': ''});

    /**
    * Resets the modal content
    * This prevents a running video to keep going in the background
    */
    if(this.options.resetOnClose) {
      this.$element.html(this.$element.html());
    }

    this.isActive = false;
    this.$element.attr({'aria-hidden': true})
    /**
     * Fires when the modal is done closing.
     * @event Reveal#closed
     */
                 .trigger('closed.zf.reveal');
  };
  /**
   * Toggles the open/closed state of a modal.
   * @function
   */
  Reveal.prototype.toggle = function(){
    if(this.isActive){
      this.close();
    }else{
      this.open();
    }
  };

  /**
   * Destroys an instance of a modal.
   * @function
   */
  Reveal.prototype.destroy = function() {
    if(this.options.overlay){
      this.$overlay.hide().off().remove();
    }
    this.$element.hide();
    this.$anchor.off();

    Foundation.unregisterPlugin(this);
  };

  Foundation.plugin(Reveal, 'Reveal');

  // Exports for AMD/Browserify
  if (typeof module !== 'undefined' && typeof module.exports !== 'undefined')
    module.exports = Reveal;
  if (typeof define === 'function')
    define(['foundation'], function() {
      return Reveal;
    });

}(Foundation, jQuery);
<|endoftext|>


<commit_before>/*jshint maxparams: 6 */
'use strict';

var LAR = require('../models/lar');

var compareYearTotals = function(newLoans, oldLoans, percentage) {
    var diff = (newLoans - oldLoans) / oldLoans;
    var result = {
        'Previous Year Total Loans': oldLoans,
        'Current Year Total Loans': newLoans,
        '% Difference': (diff*100).toFixed(2),
        'result': false,
    };
    if (isNaN(diff) || (diff > -percentage && diff < percentage)) {
        result.result = true;
    }
    return result;
};

var comparePercentages = function (newPercentage, oldPercentage, threshold, range) {
    var diff = (newPercentage - oldPercentage).toFixed(2);

    if (!range && diff < threshold) {
        return false;
    } else if (range && Math.abs(diff) >= threshold) {
        return false;
    }
    return true;
};

var checkValue = function (data, label) {
    console.log(label);
    if (data) {
        return data[label];
    } else {
        return 0;
    }
};

var calculateYearOnYearLoans = function (respondentInfo, currentLoans, currentSoldLoans,
            labels, loanParameters, callback) {
    var previousYearLoans,
        previousYearSoldLoans;

    LAR.findOne(respondentInfo).exec()
    .then(function (data) {
        previousYearLoans = checkValue(data, labels.total);
        previousYearSoldLoans = checkValue(data, labels.compare);
        var previousYearPercent = previousYearSoldLoans/previousYearLoans,
            currentPercent = currentSoldLoans/currentLoans;
        console.log(previousYearPercent + "-" + currentPercent);
        if (isNaN(previousYearPercent)) {
            previousYearPercent = 0;
        }
        if (isNaN(currentPercent)) {
            currentPercent = 0;
        }
        var result = {
            'Previous Year Loans': previousYearLoans,
            'Previous Year Sold Loans': previousYearSoldLoans,
            'Previous Year Percentage' : (previousYearPercent*100).toFixed(2),
            'Current Year Loans': currentLoans,
            'Current Year Sold Loans': currentSoldLoans,
            'Current Year Percentage' : (currentPercent*100).toFixed(2),
            '% Difference': ((currentPercent - previousYearPercent)*100).toFixed(2),
            'result': false
        };

        // diff year to year can't be more than threshold, if over largeNum,
        // then currentPercentage should be greater than minPercent
        if (loanParameters.threshold=== undefined &&
                comparePercentages(currentPercent, previousYearPercent, loanParameters.diffPercent, true)) {
            result.result = true;
        } else if (comparePercentages(currentPercent, previousYearPercent, loanParameters.diffPercent, false) &&
            ((currentLoans>=loanParameters.threshold && currentPercent> loanParameters.minPercent) || currentLoans<loanParameters.threshold)) {
            result.result = true;
        }

        return callback(null, result);

    })
    .then(null, function (err) {
        return callback(err, null);
    });
};

module.exports = {
    isValidNumHomePurchaseLoans: function(activityYear, agencyCode, respondentID, currentLoans, currentSoldLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalHomePurchaseLoans', compare: 'soldHomePurchaseLoans'};

        var loanParameters = {
            diffPercent: 0.2
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentSoldLoans,
            labels, loanParameters, callback);
    },
    isValidNumLoans: function(activityYear, agencyCode, respondentID, newLoans, callback) {
        activityYear -= 1;
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        LAR.findOne(respondentInfo, function(err, previousYearTotals) {
            if (err) {
                return callback(err, null);
            }
            // Return a passing result if neither year has >= 500 loans
            if (previousYearTotals.totalLoans < 500 && newLoans < 500) {
                return callback(null, {'result': true});
            }
            var result = compareYearTotals(newLoans, previousYearTotals.totalLoans, 0.2);
            return callback(null, result);
        });
    },
    isValidNumRefinanceLoans: function(activityYear, agencyCode, respondentID, currentLoans, currentSoldLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalRefinanceLoans', compare: 'soldRefinanceLoans'};

        var loanParameters = {
            diffPercent: 0.2
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentSoldLoans,
            labels, loanParameters, callback);
    },
    isValidNumFannieLoans: function(activityYear, agencyCode, respondentID, currentLoans, currentFannieLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalQ70', compare: 'compareQ70'};

        var loanParameters = {
            diffPercent: -0.1,
            threshold: 10000,
            minPercent: 0.2
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentFannieLoans,
            labels, loanParameters, callback);
    },
    isValidNumGinnieMaeFHALoans: function(activityYear, agencyCode, respondentID, currentLoans, currentGinnieLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalQ71', compare: 'compareQ71'};

        var loanParameters = {
            diffPercent: -0.1,
            threshold: 2500,
            minPercent: 0.3
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentGinnieLoans,
            labels, loanParameters, callback);
    },
    isValidNumGinnieMaeVALoans: function(activityYear, agencyCode, respondentID, currentLoans, currentGinnieLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalQ72', compare: 'compareQ72'};

        var loanParameters = {
            diffPercent: -0.1,
            threshold: 2000,
            minPercent: 0.3
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentGinnieLoans,
            labels, loanParameters, callback);
    }
};<commit_msg>fixed single quote<commit_after>/*jshint maxparams: 6 */
'use strict';

var LAR = require('../models/lar');

var compareYearTotals = function(newLoans, oldLoans, percentage) {
    var diff = (newLoans - oldLoans) / oldLoans;
    var result = {
        'Previous Year Total Loans': oldLoans,
        'Current Year Total Loans': newLoans,
        '% Difference': (diff*100).toFixed(2),
        'result': false,
    };
    if (isNaN(diff) || (diff > -percentage && diff < percentage)) {
        result.result = true;
    }
    return result;
};

var comparePercentages = function (newPercentage, oldPercentage, threshold, range) {
    var diff = (newPercentage - oldPercentage).toFixed(2);

    if (!range && diff < threshold) {
        return false;
    } else if (range && Math.abs(diff) >= threshold) {
        return false;
    }
    return true;
};

var checkValue = function (data, label) {
    console.log(label);
    if (data) {
        return data[label];
    } else {
        return 0;
    }
};

var calculateYearOnYearLoans = function (respondentInfo, currentLoans, currentSoldLoans,
            labels, loanParameters, callback) {
    var previousYearLoans,
        previousYearSoldLoans;

    LAR.findOne(respondentInfo).exec()
    .then(function (data) {
        previousYearLoans = checkValue(data, labels.total);
        previousYearSoldLoans = checkValue(data, labels.compare);
        var previousYearPercent = previousYearSoldLoans/previousYearLoans,
            currentPercent = currentSoldLoans/currentLoans;
        console.log(previousYearPercent + '-' + currentPercent);
        if (isNaN(previousYearPercent)) {
            previousYearPercent = 0;
        }
        if (isNaN(currentPercent)) {
            currentPercent = 0;
        }
        var result = {
            'Previous Year Loans': previousYearLoans,
            'Previous Year Sold Loans': previousYearSoldLoans,
            'Previous Year Percentage' : (previousYearPercent*100).toFixed(2),
            'Current Year Loans': currentLoans,
            'Current Year Sold Loans': currentSoldLoans,
            'Current Year Percentage' : (currentPercent*100).toFixed(2),
            '% Difference': ((currentPercent - previousYearPercent)*100).toFixed(2),
            'result': false
        };

        // diff year to year can't be more than threshold, if over largeNum,
        // then currentPercentage should be greater than minPercent
        if (loanParameters.threshold=== undefined &&
                comparePercentages(currentPercent, previousYearPercent, loanParameters.diffPercent, true)) {
            result.result = true;
        } else if (comparePercentages(currentPercent, previousYearPercent, loanParameters.diffPercent, false) &&
            ((currentLoans>=loanParameters.threshold && currentPercent> loanParameters.minPercent) || currentLoans<loanParameters.threshold)) {
            result.result = true;
        }

        return callback(null, result);

    })
    .then(null, function (err) {
        return callback(err, null);
    });
};

module.exports = {
    isValidNumHomePurchaseLoans: function(activityYear, agencyCode, respondentID, currentLoans, currentSoldLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalHomePurchaseLoans', compare: 'soldHomePurchaseLoans'};

        var loanParameters = {
            diffPercent: 0.2
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentSoldLoans,
            labels, loanParameters, callback);
    },
    isValidNumLoans: function(activityYear, agencyCode, respondentID, newLoans, callback) {
        activityYear -= 1;
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        LAR.findOne(respondentInfo, function(err, previousYearTotals) {
            if (err) {
                return callback(err, null);
            }
            // Return a passing result if neither year has >= 500 loans
            if (previousYearTotals.totalLoans < 500 && newLoans < 500) {
                return callback(null, {'result': true});
            }
            var result = compareYearTotals(newLoans, previousYearTotals.totalLoans, 0.2);
            return callback(null, result);
        });
    },
    isValidNumRefinanceLoans: function(activityYear, agencyCode, respondentID, currentLoans, currentSoldLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalRefinanceLoans', compare: 'soldRefinanceLoans'};

        var loanParameters = {
            diffPercent: 0.2
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentSoldLoans,
            labels, loanParameters, callback);
    },
    isValidNumFannieLoans: function(activityYear, agencyCode, respondentID, currentLoans, currentFannieLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalQ70', compare: 'compareQ70'};

        var loanParameters = {
            diffPercent: -0.1,
            threshold: 10000,
            minPercent: 0.2
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentFannieLoans,
            labels, loanParameters, callback);
    },
    isValidNumGinnieMaeFHALoans: function(activityYear, agencyCode, respondentID, currentLoans, currentGinnieLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalQ71', compare: 'compareQ71'};

        var loanParameters = {
            diffPercent: -0.1,
            threshold: 2500,
            minPercent: 0.3
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentGinnieLoans,
            labels, loanParameters, callback);
    },
    isValidNumGinnieMaeVALoans: function(activityYear, agencyCode, respondentID, currentLoans, currentGinnieLoans, callback) {
        activityYear -= 1;
        var labels = {total:'totalQ72', compare: 'compareQ72'};

        var loanParameters = {
            diffPercent: -0.1,
            threshold: 2000,
            minPercent: 0.3
        };
        var respondentInfo = {
            'activity_year': activityYear,
            'respondent_id': respondentID,
            'agency_code': agencyCode
        };

        return calculateYearOnYearLoans (respondentInfo, currentLoans, currentGinnieLoans,
            labels, loanParameters, callback);
    }
};<|endoftext|>


<commit_before>const autoprefixer = require('autoprefixer');
const beml = require('gulp-beml');
const csso = require('postcss-csso');
const gulp = require('gulp');
const htmlmin = require('gulp-htmlmin');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');
const sync = require('browser-sync').create();

gulp.task('styles', () => {
	return gulp.src('src/styles/screen.scss')
		.pipe(sass().on('error', sass.logError))
		.pipe(postcss([
			autoprefixer,
			csso
		]))
		.pipe(gulp.dest('dest/styles'))
		.pipe(sync.stream());
});

gulp.task('html', () => {
	return gulp.src('src/index.html')
		.pipe(beml({
			elemPrefix: '__',
			modPrefix: '--' }))
		.pipe(htmlmin({
			removeComments: true,
			collapseWhitespace: true
		}))
		.pipe(gulp.dest('dest'))
		.pipe(sync.stream());
});

gulp.task('copy', () => {
	return gulp.src('src/assets/**', { dot: true })
		.pipe(gulp.dest('dest'))
		.pipe(sync.stream({ once: true }));
});

gulp.task('server', () => {
	sync.init({
		notify: false,
		server: {
			baseDir: 'dest'
		}
	});
});

gulp.task('watch', () => {
	gulp.watch('src/styles/*.scss', ['styles']);
	gulp.watch('src/index.html', ['html']);
	gulp.watch('src/assets/**', ['copy']);
});

gulp.task('build', [
	'styles',
	'html',
	'copy'
]);

gulp.task('default', [
	'build',
	'server',
	'watch'
]);
<commit_msg>Replacing canonical URL for local development<commit_after>const autoprefixer = require('autoprefixer');
const beml = require('gulp-beml');
const csso = require('postcss-csso');
const gulp = require('gulp');
const htmlmin = require('gulp-htmlmin');
const postcss = require('gulp-postcss');
const sass = require('gulp-sass');
const sync = require('browser-sync').create();

gulp.task('styles', () => {
	return gulp.src('src/styles/screen.scss')
		.pipe(sass().on('error', sass.logError))
		.pipe(postcss([
			autoprefixer,
			csso
		]))
		.pipe(gulp.dest('dest/styles'))
		.pipe(sync.stream());
});

gulp.task('html', () => {
	return gulp.src('src/index.html')
		.pipe(beml({
			elemPrefix: '__',
			modPrefix: '--' }))
		.pipe(htmlmin({
			removeComments: true,
			collapseWhitespace: true
		}))
		.pipe(gulp.dest('dest'))
		.pipe(sync.stream());
});

gulp.task('copy', () => {
	return gulp.src('src/assets/**', { dot: true })
		.pipe(gulp.dest('dest'))
		.pipe(sync.stream({ once: true }));
});

gulp.task('server', () => {
	sync.init({
		notify: false,
		server: {
			baseDir: 'dest'
		},
		rewriteRules: [{
			match: /<link rel="canonical" href="https:\/\/pwa\.rocks\/">/g,
	        fn: function (match) {
				return '';
	        }
		}]
	});
});

gulp.task('watch', () => {
	gulp.watch('src/styles/*.scss', ['styles']);
	gulp.watch('src/index.html', ['html']);
	gulp.watch('src/assets/**', ['copy']);
});

gulp.task('build', [
	'styles',
	'html',
	'copy'
]);

gulp.task('default', [
	'build',
	'server',
	'watch'
]);
<|endoftext|>


<commit_before>'use strict';

var request = require('sync-request');

//de modificat
//const API_HOST = process.env.UI_API_URL;
const API_HOST = 'https://lapachet-app.herokuapp.com/api';
const PRODUCTS_URL = '/category_products?date={date}&categoryId={categoryId}';
const PRODUCT_URL = '/products/{productId}';
const MENU_URL = '/products_menus?date={date}';
const CATEGORIES_URL = '/categories/daily?date={date}';
const INTERVALS_URL = '/intervals?date={date}';
const SUBSCRIBE_USER_URL = '/users';
const REGISTER_ORDER_URL = '/sales';

const HTTP_REQUEST_GET = 'GET';
const HTTP_REQUEST_POST = 'POST';

function ApiService() {
}

ApiService.prototype = {
    getSellingProducts: function (categoryId) {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = PRODUCTS_URL.replace('{date}', date);
        url = url.replace('{categoryId}', categoryId);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getProduct: function (productId) {
        var url = PRODUCT_URL.replace('{productId}', productId);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getSellingMenus: function (date) {
        var url = MENU_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getSellingProductCategories: function () {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = CATEGORIES_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getIntervals: function () {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = INTERVALS_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getUrl: function (url) {
        return API_HOST + url;
    },
    request: function (method, url, options) {

        var requestUrl = this.getUrl(url);
        var res = request(method, requestUrl, options);

        console.info("API CALL: ", requestUrl);
        console.info("RSS:", res.body);
        return JSON.parse(res.body('utf-8').toString('utf-8'));
    },
    registerOrder: function (productId, intervalId, userId) {
        var date = new Date().toISOString().replace(/T.+/, '');
        var req = this.request(
            HTTP_REQUEST_POST,
            REGISTER_ORDER_URL,
            {
                json: {
                    userId: userId,
                    productId: productId,
                    intervalId: intervalId,
                    date: date
                }
            }
        );
        console.log(req);
        return req;
    }
};

module.exports.getInstance = function () {
    return new ApiService();
};<commit_msg>add notifier<commit_after>'use strict';

var request = require('sync-request');

//de modificat
//const API_HOST = process.env.UI_API_URL;
const API_HOST = 'https://lapachet-app.herokuapp.com/api';
const PRODUCTS_URL = '/category_products?date={date}&categoryId={categoryId}';
const PRODUCT_URL = '/products/{productId}';
const MENU_URL = '/products_menus?date={date}';
const CATEGORIES_URL = '/categories/daily?date={date}';
const INTERVALS_URL = '/intervals?date={date}';
const SUBSCRIBE_USER_URL = '/users';
const REGISTER_ORDER_URL = '/sales';

const HTTP_REQUEST_GET = 'GET';
const HTTP_REQUEST_POST = 'POST';

function ApiService() {
}

ApiService.prototype = {
    getSellingProducts: function (categoryId) {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = PRODUCTS_URL.replace('{date}', date);
        url = url.replace('{categoryId}', categoryId);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getProduct: function (productId) {
        var url = PRODUCT_URL.replace('{productId}', productId);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getSellingMenus: function (date) {
        var url = MENU_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getSellingProductCategories: function () {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = CATEGORIES_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getIntervals: function () {
        var date = new Date().toISOString().replace(/T.+/, '');
        var url = INTERVALS_URL.replace('{date}', date);

        return this.request(HTTP_REQUEST_GET, url);
    },
    getUrl: function (url) {
        return API_HOST + url;
    },
    request: function (method, url, options) {

        var requestUrl = this.getUrl(url);
        var res = request(method, requestUrl, options);

        console.info("API CALL: ", requestUrl);
        console.info("RSS:", res.body.toString('utf-8'));
        return JSON.parse(res.body.toString('utf-8'));
    },
    registerOrder: function (productId, intervalId, userId) {
        var date = new Date().toISOString().replace(/T.+/, '');
        var req = this.request(
            HTTP_REQUEST_POST,
            REGISTER_ORDER_URL,
            {
                json: {
                    userId: userId,
                    productId: productId,
                    intervalId: intervalId,
                    date: date
                }
            }
        );
        console.log(req);
        return req;
    }
};

module.exports.getInstance = function () {
    return new ApiService();
};<|endoftext|>