<commit_before>BR.Export = L.Class.extend({
    latLngs: [],

    options: {
        shortcut: {
            export: 88, // char code for 'x'
        },
    },

    initialize: function (router, pois, profile) {
        this.router = router;
        this.pois = pois;
        this.profile = profile;
        this.exportButton = $('#exportButton');
        var trackname = (this.trackname = document.getElementById('trackname'));
        this.tracknameAllowedChars = BR.conf.tracknameAllowedChars;

        // a.download attribute automatically replaces invalid characters
        if (!BR.Browser.download && this.tracknameAllowedChars) {
            this.tracknameMessage = document.getElementById('trackname-message');
            var patternRegex = new RegExp('[' + this.tracknameAllowedChars + ']+');

            // warn about special characters getting removed by server quick fix (#194)
            trackname.pattern = patternRegex.toString().slice(1, -1);
            trackname.addEventListener('input', L.bind(this._validationMessage, this));
        }

        this.exportButton.on('click', L.bind(this._generateTrackname, this));
        L.DomUtil.get('submitExport').onclick = L.bind(this._export, this);
        L.DomUtil.get('serverExport').onclick = L.bind(this._exportServer, this);

        L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);

        $('#export').on('show.bs.modal', this._warnStraightLine.bind(this));

        this.update([]);
    },

    update: function (latLngs, segments) {
        this.latLngs = latLngs;
        this.segments = segments;

        if (latLngs.length < 2) {
            this.exportButton.addClass('disabled');
        } else {
            this.exportButton.removeClass('disabled');
        }
    },

    _warnStraightLine: function () {
        const hasBeeline = BR.Routing.hasBeeline(this.segments);
        document.getElementById('export-beeline-warning').hidden = !hasBeeline;
        let title = 'Download from server (deprecated)';
        if (hasBeeline) {
            title = '[Warning: straight lines not supported] ' + title;
        }
        document.getElementById('serverExport').title = title;
    },

    _getMimeType: function (format) {
        const mimeTypeMap = {
            gpx: 'application/gpx+xml',
            kml: 'application/vnd.google-earth.kml+xml',
            geojson: 'application/vnd.geo+json',
            csv: 'text/tab-separated-values',
        };

        return mimeTypeMap[format];
    },

    _triggerDownload: function (url, name) {
        const link = document.createElement('a');
        link.href = url;
        if (name) {
            link.download = name;
        }
        link.click();
    },

    _exportServer: function (e) {
        this._export(e, true);
    },

    _export: function (e, server = false) {
        var exportForm = document.forms['export'];
        var format = exportForm['format'].value || $('#export-format input:radio:checked').val();
        var name = exportForm['trackname'].value;
        var nameUri = encodeURIComponent(name);
        var includeWaypoints = exportForm['include-waypoints'].checked;

        e.preventDefault();

        if (!server && BR.Browser.download) {
            const track = this._formatTrack(format, name, includeWaypoints);
            const fileName = (name || 'brouter') + '.' + format;

            const mimeType = this._getMimeType(format);
            const blob = new Blob([track], {
                type: mimeType + ';charset=utf-8',
            });

            const reader = new FileReader();
            reader.onload = (e) => this._triggerDownload(reader.result, fileName);
            reader.readAsDataURL(blob);
        } else {
            var serverUrl = this.router.getUrl(
                this.latLngs,
                null,
                this.pois.getMarkers(),
                null,
                format,
                nameUri,
                includeWaypoints
            );

            this._triggerDownload(serverUrl);
        }
    },

    _formatTrack: function (format, name, includeWaypoints) {
        const track = BR.Export._concatTotalTrack(this.segments);
        if (name) {
            track.features[0].properties.name = name;
        }
        this._addPois(track);
        if (includeWaypoints) {
            this._addRouteWaypoints(track);
        }
        switch (format) {
            case 'gpx':
                const turnInstructionMode = +this.profile.getProfileVar('turnInstructionMode');
                const transportMode = this.profile.getTransportMode();
                return BR.Gpx.format(track, turnInstructionMode, transportMode);
            case 'kml':
                return BR.Kml.format(track);
            case 'geojson':
                return JSON.stringify(track, null, 2);
            case 'csv':
                return BR.Csv.format(track);
            default:
                break;
        }
        console.error('Export format not implemented: ' + format);
    },

    _addPois: function (track) {
        const markers = this.pois.getMarkers();
        for (const poi of markers) {
            const properties = { name: poi.name, type: 'poi' };
            const point = turf.point([poi.latlng.lng, poi.latlng.lat], properties);
            track.features.push(point);
        }
    },

    _addRouteWaypoints: function (track) {
        for (const [i, latLng] of this.latLngs.entries()) {
            let name = 'via' + i;
            let type = 'via';
            if (i === 0) {
                name = 'from';
                type = 'from';
            } else if (i === this.latLngs.length - 1) {
                name = 'to';
                type = 'to';
            }
            const properties = { name, type };
            const point = turf.point([latLng.lng, latLng.lat], properties);
            track.features.push(point);
        }
    },

    _validationMessage: function () {
        var trackname = this.trackname;
        var replaceRegex = new RegExp('[^' + this.tracknameAllowedChars + ']', 'g');

        if (trackname.validity.patternMismatch) {
            var replaced = trackname.value.replace(replaceRegex, '');
            var patternStr = this.tracknameAllowedChars.replace(/\\/g, '');
            this.tracknameMessage.textContent = '[' + patternStr + '] --> ' + replaced;
        } else {
            this.tracknameMessage.textContent = '';
        }
    },

    _selectTrackname: function () {
        trackname.setSelectionRange(0, trackname.value.lastIndexOf(BR.Browser.download ? ' (' : ' - '));
    },

    _generateTrackname: function () {
        var trackname = this.trackname;
        this._getCityAtPosition(
            this.latLngs[0],
            L.bind(function (from) {
                this._getCityAtPosition(
                    this.latLngs[this.latLngs.length - 1],
                    L.bind(function (to) {
                        var distance = document.getElementById('distance').innerHTML;
                        if (!BR.Browser.download && this.tracknameAllowedChars) {
                            distance = distance.replace(',', '.'); // temp. fix (#202)
                        }
                        if (!from || !to) {
                            trackname.value = null;
                        } else if (from === to) {
                            trackname.value = i18next.t('export.route-loop', {
                                from: from,
                                distance: distance,
                            });
                        } else {
                            trackname.value = i18next.t('export.route-from-to', {
                                from: from,
                                to: to,
                                distance: distance,
                            });
                        }

                        if (!BR.Browser.download && this.tracknameAllowedChars) {
                            // temp. fix: replace and remove characters that will get removed by server quick fix (#194)
                            trackname.value = trackname.value.replace(/[>)]/g, '').replace(/ \(/g, ' - ');
                            this._validationMessage();
                        }

                        this._selectTrackname();
                    }, this)
                );
            }, this)
        );
    },

    _getCityAtPosition: function (lonlat, cb) {
        var url = L.Util.template(
            'https://nominatim.openstreetmap.org/reverse?lon={lng}&lat={lat}&format=json',
            lonlat
        );
        BR.Util.get(
            url,
            L.bind(function (err, response) {
                try {
                    var addr = JSON.parse(response).address;
                    cb(addr.village || addr.town || addr.hamlet || addr.city_district || addr.city);
                } catch (err) {
                    BR.message.showError('Error getting position city "' + lonlat + '": ' + err);
                    return cb(null);
                }
            })
        );
    },

    _keydownListener: function (e) {
        if (
            BR.Util.keyboardShortcutsAllowed(e) &&
            e.keyCode === this.options.shortcut.export &&
            !this.exportButton.hasClass('disabled')
        ) {
            this._generateTrackname();
            $('#export').modal('show');
        }
    },
});

BR.export = function () {
    return new BR.Export();
};

BR.Export._concatTotalTrack = function (segments) {
    const sumProperties = (p, fp, keys) => {
        for (const key of keys) {
            p[key] = (+p[key] + +fp[key]).toString();
        }
    };
    let coordinates = [];
    let properties;

    for (const [segmentIndex, segment] of segments.entries()) {
        const feature = segment.feature;
        if (!feature) continue;

        const coordOffset = coordinates.length > 0 ? coordinates.length - 1 : 0;
        if (properties) {
            const p = properties;
            const fp = feature.properties;

            sumProperties(p, fp, [
                'cost',
                'filtered ascend',
                'plain-ascend',
                'total-energy',
                'total-time',
                'track-length',
            ]);

            p.messages = p.messages.concat(fp.messages.slice(1));
            if (p.times && fp.times) {
                const lastTime = p.times[p.times.length - 1];
                for (const [timeIndex, time] of fp.times.entries()) {
                    if (timeIndex > 0) {
                        p.times.push(+(lastTime + time).toFixed(3));
                    }
                }
            }
            if (fp.voicehints) {
                if (!p.voicehints) p.voicehints = [];
                for (const fpHint of fp.voicehints) {
                    const hint = fpHint.slice();
                    hint[0] += coordOffset;
                    p.voicehints.push(hint);
                }
            }
        } else {
            // clone
            properties = Object.assign({}, feature.properties);
            if (properties.voicehints) {
                properties.voicehints = properties.voicehints.slice();
            }
            if (properties.times) {
                properties.times = properties.times.slice();
            }
        }

        let featureCoordinates = feature.geometry.coordinates;
        if (segmentIndex > 0) {
            // remove duplicate coordinate: first segment coordinate same as previous last,
            // remove the one without ele value (e.g. beeline)
            const prevLast = coordinates[coordinates.length - 1];
            const first = featureCoordinates[0];
            if (prevLast.length < first.length) {
                coordinates.pop();
            } else {
                featureCoordinates = featureCoordinates.slice(1);
            }
        }
        coordinates = coordinates.concat(featureCoordinates);
    }

    return turf.featureCollection([turf.lineString(coordinates, properties)]);
};
<commit_msg>Fix download click for Firefox < 75 (attach to DOM)<commit_after>BR.Export = L.Class.extend({
    latLngs: [],

    options: {
        shortcut: {
            export: 88, // char code for 'x'
        },
    },

    initialize: function (router, pois, profile) {
        this.router = router;
        this.pois = pois;
        this.profile = profile;
        this.exportButton = $('#exportButton');
        var trackname = (this.trackname = document.getElementById('trackname'));
        this.tracknameAllowedChars = BR.conf.tracknameAllowedChars;

        // a.download attribute automatically replaces invalid characters
        if (!BR.Browser.download && this.tracknameAllowedChars) {
            this.tracknameMessage = document.getElementById('trackname-message');
            var patternRegex = new RegExp('[' + this.tracknameAllowedChars + ']+');

            // warn about special characters getting removed by server quick fix (#194)
            trackname.pattern = patternRegex.toString().slice(1, -1);
            trackname.addEventListener('input', L.bind(this._validationMessage, this));
        }

        this.exportButton.on('click', L.bind(this._generateTrackname, this));
        L.DomUtil.get('submitExport').onclick = L.bind(this._export, this);
        L.DomUtil.get('serverExport').onclick = L.bind(this._exportServer, this);

        L.DomEvent.addListener(document, 'keydown', this._keydownListener, this);

        $('#export').on('show.bs.modal', this._warnStraightLine.bind(this));

        this.update([]);
    },

    update: function (latLngs, segments) {
        this.latLngs = latLngs;
        this.segments = segments;

        if (latLngs.length < 2) {
            this.exportButton.addClass('disabled');
        } else {
            this.exportButton.removeClass('disabled');
        }
    },

    _warnStraightLine: function () {
        const hasBeeline = BR.Routing.hasBeeline(this.segments);
        document.getElementById('export-beeline-warning').hidden = !hasBeeline;
        let title = 'Download from server (deprecated)';
        if (hasBeeline) {
            title = '[Warning: straight lines not supported] ' + title;
        }
        document.getElementById('serverExport').title = title;
    },

    _getMimeType: function (format) {
        const mimeTypeMap = {
            gpx: 'application/gpx+xml',
            kml: 'application/vnd.google-earth.kml+xml',
            geojson: 'application/vnd.geo+json',
            csv: 'text/tab-separated-values',
        };

        return mimeTypeMap[format];
    },

    _triggerDownload: function (url, name) {
        const link = document.createElement('a');
        link.href = url;
        if (name) {
            link.download = name;
        }
        link.hidden = true;
        // in Firefox < 75 click() only works when element is attached to DOM tree
        document.body.appendChild(link);
        link.click();
        link.remove();
    },

    _exportServer: function (e) {
        this._export(e, true);
    },

    _export: function (e, server = false) {
        var exportForm = document.forms['export'];
        var format = exportForm['format'].value || $('#export-format input:radio:checked').val();
        var name = exportForm['trackname'].value;
        var nameUri = encodeURIComponent(name);
        var includeWaypoints = exportForm['include-waypoints'].checked;

        e.preventDefault();

        if (!server && BR.Browser.download) {
            const track = this._formatTrack(format, name, includeWaypoints);
            const fileName = (name || 'brouter') + '.' + format;

            const mimeType = this._getMimeType(format);
            const blob = new Blob([track], {
                type: mimeType + ';charset=utf-8',
            });

            const reader = new FileReader();
            reader.onload = (e) => this._triggerDownload(reader.result, fileName);
            reader.readAsDataURL(blob);
        } else {
            var serverUrl = this.router.getUrl(
                this.latLngs,
                null,
                this.pois.getMarkers(),
                null,
                format,
                nameUri,
                includeWaypoints
            );

            this._triggerDownload(serverUrl);
        }
    },

    _formatTrack: function (format, name, includeWaypoints) {
        const track = BR.Export._concatTotalTrack(this.segments);
        if (name) {
            track.features[0].properties.name = name;
        }
        this._addPois(track);
        if (includeWaypoints) {
            this._addRouteWaypoints(track);
        }
        switch (format) {
            case 'gpx':
                const turnInstructionMode = +this.profile.getProfileVar('turnInstructionMode');
                const transportMode = this.profile.getTransportMode();
                return BR.Gpx.format(track, turnInstructionMode, transportMode);
            case 'kml':
                return BR.Kml.format(track);
            case 'geojson':
                return JSON.stringify(track, null, 2);
            case 'csv':
                return BR.Csv.format(track);
            default:
                break;
        }
        console.error('Export format not implemented: ' + format);
    },

    _addPois: function (track) {
        const markers = this.pois.getMarkers();
        for (const poi of markers) {
            const properties = { name: poi.name, type: 'poi' };
            const point = turf.point([poi.latlng.lng, poi.latlng.lat], properties);
            track.features.push(point);
        }
    },

    _addRouteWaypoints: function (track) {
        for (const [i, latLng] of this.latLngs.entries()) {
            let name = 'via' + i;
            let type = 'via';
            if (i === 0) {
                name = 'from';
                type = 'from';
            } else if (i === this.latLngs.length - 1) {
                name = 'to';
                type = 'to';
            }
            const properties = { name, type };
            const point = turf.point([latLng.lng, latLng.lat], properties);
            track.features.push(point);
        }
    },

    _validationMessage: function () {
        var trackname = this.trackname;
        var replaceRegex = new RegExp('[^' + this.tracknameAllowedChars + ']', 'g');

        if (trackname.validity.patternMismatch) {
            var replaced = trackname.value.replace(replaceRegex, '');
            var patternStr = this.tracknameAllowedChars.replace(/\\/g, '');
            this.tracknameMessage.textContent = '[' + patternStr + '] --> ' + replaced;
        } else {
            this.tracknameMessage.textContent = '';
        }
    },

    _selectTrackname: function () {
        trackname.setSelectionRange(0, trackname.value.lastIndexOf(BR.Browser.download ? ' (' : ' - '));
    },

    _generateTrackname: function () {
        var trackname = this.trackname;
        this._getCityAtPosition(
            this.latLngs[0],
            L.bind(function (from) {
                this._getCityAtPosition(
                    this.latLngs[this.latLngs.length - 1],
                    L.bind(function (to) {
                        var distance = document.getElementById('distance').innerHTML;
                        if (!BR.Browser.download && this.tracknameAllowedChars) {
                            distance = distance.replace(',', '.'); // temp. fix (#202)
                        }
                        if (!from || !to) {
                            trackname.value = null;
                        } else if (from === to) {
                            trackname.value = i18next.t('export.route-loop', {
                                from: from,
                                distance: distance,
                            });
                        } else {
                            trackname.value = i18next.t('export.route-from-to', {
                                from: from,
                                to: to,
                                distance: distance,
                            });
                        }

                        if (!BR.Browser.download && this.tracknameAllowedChars) {
                            // temp. fix: replace and remove characters that will get removed by server quick fix (#194)
                            trackname.value = trackname.value.replace(/[>)]/g, '').replace(/ \(/g, ' - ');
                            this._validationMessage();
                        }

                        this._selectTrackname();
                    }, this)
                );
            }, this)
        );
    },

    _getCityAtPosition: function (lonlat, cb) {
        var url = L.Util.template(
            'https://nominatim.openstreetmap.org/reverse?lon={lng}&lat={lat}&format=json',
            lonlat
        );
        BR.Util.get(
            url,
            L.bind(function (err, response) {
                try {
                    var addr = JSON.parse(response).address;
                    cb(addr.village || addr.town || addr.hamlet || addr.city_district || addr.city);
                } catch (err) {
                    BR.message.showError('Error getting position city "' + lonlat + '": ' + err);
                    return cb(null);
                }
            })
        );
    },

    _keydownListener: function (e) {
        if (
            BR.Util.keyboardShortcutsAllowed(e) &&
            e.keyCode === this.options.shortcut.export &&
            !this.exportButton.hasClass('disabled')
        ) {
            this._generateTrackname();
            $('#export').modal('show');
        }
    },
});

BR.export = function () {
    return new BR.Export();
};

BR.Export._concatTotalTrack = function (segments) {
    const sumProperties = (p, fp, keys) => {
        for (const key of keys) {
            p[key] = (+p[key] + +fp[key]).toString();
        }
    };
    let coordinates = [];
    let properties;

    for (const [segmentIndex, segment] of segments.entries()) {
        const feature = segment.feature;
        if (!feature) continue;

        const coordOffset = coordinates.length > 0 ? coordinates.length - 1 : 0;
        if (properties) {
            const p = properties;
            const fp = feature.properties;

            sumProperties(p, fp, [
                'cost',
                'filtered ascend',
                'plain-ascend',
                'total-energy',
                'total-time',
                'track-length',
            ]);

            p.messages = p.messages.concat(fp.messages.slice(1));
            if (p.times && fp.times) {
                const lastTime = p.times[p.times.length - 1];
                for (const [timeIndex, time] of fp.times.entries()) {
                    if (timeIndex > 0) {
                        p.times.push(+(lastTime + time).toFixed(3));
                    }
                }
            }
            if (fp.voicehints) {
                if (!p.voicehints) p.voicehints = [];
                for (const fpHint of fp.voicehints) {
                    const hint = fpHint.slice();
                    hint[0] += coordOffset;
                    p.voicehints.push(hint);
                }
            }
        } else {
            // clone
            properties = Object.assign({}, feature.properties);
            if (properties.voicehints) {
                properties.voicehints = properties.voicehints.slice();
            }
            if (properties.times) {
                properties.times = properties.times.slice();
            }
        }

        let featureCoordinates = feature.geometry.coordinates;
        if (segmentIndex > 0) {
            // remove duplicate coordinate: first segment coordinate same as previous last,
            // remove the one without ele value (e.g. beeline)
            const prevLast = coordinates[coordinates.length - 1];
            const first = featureCoordinates[0];
            if (prevLast.length < first.length) {
                coordinates.pop();
            } else {
                featureCoordinates = featureCoordinates.slice(1);
            }
        }
        coordinates = coordinates.concat(featureCoordinates);
    }

    return turf.featureCollection([turf.lineString(coordinates, properties)]);
};
<|endoftext|>


<commit_before>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2014, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * This program 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 License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

/* jshint esversion:6 */

var _ = require('lodash');

var format = require('./util/format');
var dt = require('./util/datetime');

var MS_IN_HOUR = 3600000;
var MS_IN_DAY = 86400000;

function BasalUtil(data) {

  this.scheduleTotal = function(schedule) {
    if ((! (schedule && Array.isArray(schedule) && schedule.length > 0))) {
      return NaN;
    }
    var total = 0, duration;
    schedule.push({
      start: MS_IN_DAY
    });
    for (var i = 1; i <= schedule.length - 1; i++) {
      duration = schedule[i].start - schedule[i - 1].start;
      total += this.segmentDose(duration, schedule[i - 1].rate);
    }

    schedule.pop();

    return format.fixFloatingPoint(total);
  };

  this.segmentDose = function(duration, rate) {
    var hours = duration / MS_IN_HOUR;
    return format.fixFloatingPoint(hours * rate);
  };

  this.subtotal = function(endpoints) {
    var dose = 0.0;
    var start = new Date(endpoints.start.datetime), end = new Date(endpoints.end.datetime);
    // handle first segment, which may have started before the start endpoint
    var segment = this.actual[endpoints.start.index];
    dose += this.segmentDose((new Date(segment.normalEnd) - start), segment.rate);
    var i = endpoints.start.index + 1;
    while (i < endpoints.end.index) {
      segment = this.actual[i];
      dose += this.segmentDose(segment.duration, segment.rate);
      i++;
    }
    segment = this.actual[endpoints.end.index];
    // handle last segment, which may go past the end endpoint
    dose += this.segmentDose((end - new Date(segment.normalTime)), segment.rate);
    return format.fixFloatingPoint(dose);
  };

  this.isContinuous = function(s, e) {
    var start = new Date(s), end = new Date(e);
    var startIndex = _.findIndex(this.actual, function(segment) {
        return (new Date(segment.normalTime).valueOf() <= start) && (start <= new Date(segment.normalEnd).valueOf());
      });
    var endIndex = _.findIndex(this.actual, function(segment) {
        return (new Date(segment.normalTime).valueOf() <= end) && (end <= new Date(segment.normalEnd).valueOf());
      });
    if ((startIndex >= 0) && (endIndex >= 0)) {
      var i = startIndex;
      while (i < endIndex) {
        var s1 = this.actual[i], s2 = this.actual[i + 1];
        if (s1.normalEnd !== s2.normalTime) {
          return false;
        }
        i++;
      }
      return {
        start: {
          datetime: start.toISOString(),
          index: startIndex
        },
        end: {
          datetime: end.toISOString(),
          index: endIndex
        }
      };
    }
    else {
      return false;
    }
  };

  this.totalBasal = function(s, e, opts) {
    opts = opts || {};
    if (dt.verifyEndpoints(s, e, this.endpoints)) {
      var endpoints;
      if (dt.isTwentyFourHours(s, e)) {
        endpoints = this.isContinuous(s, e);
        if (endpoints) {
          return {total: this.subtotal(endpoints)};
        }
        else {
          return {total: NaN};
        }
      }
      else if (dt.isLessThanTwentyFourHours(s, e)) {
        return {total: NaN};
      }
      else {
        var dose = 0.0;
        var excluded = [];
        var start = new Date(s), end = new Date(e);
        var n = dt.getNumDays(s, e);
        for (var j = 0; j < n; j++) {
          var dayStart = new Date(start);
          var dayEnd = new Date(dayStart);
          dayEnd.setUTCDate(dayEnd.getUTCDate() + 1);
          endpoints = this.isContinuous(dayStart.toISOString(), dayEnd.toISOString());
          if (endpoints && dt.isTwentyFourHours(dayStart.toISOString(), dayEnd.toISOString())) {
            if (isNaN(this.subtotal(endpoints))) {
              excluded.push(dayStart.toISOString());
            }
            else {
              dose += this.subtotal(endpoints);
            }
          }
          else {
            excluded.push(dayStart.toISOString());
          }
          start.setUTCDate(start.getUTCDate() + 1);
        }
        if (excluded.length <= opts.exclusionThreshold) {
          return {
            total: dose,
            excluded: excluded
          };
        }
        else {
          return {
            total: NaN,
            excluded: excluded
          };
        }
      }
    }
    else {
      return {total: NaN};
    }
  };

  /**
   * getBasalPathGroupType
   * @param {Object} basal - single basal datum
   * @return {String} the path group type
   */
  this.getBasalPathGroupType = function(datum) {
    return _.get(datum, 'deliveryType') === 'automated' ? 'automated' : 'regular';
  };

  /**
   * getBasalPathGroups
   * @param {Array} basals - Array of preprocessed Tidepool basal objects
   * @return {Array} groups of alternating 'automated' and 'regular' datums
   */
  this.getBasalPathGroups = function(basals) {
    var basalPathGroups = [];
    var currentPathType;
    _.each(basals, datum => {
      var pathType = this.getBasalPathGroupType(datum);
      if (pathType !== currentPathType) {
        currentPathType = pathType;
        basalPathGroups.push([]);
      }
      _.last(basalPathGroups).push(datum);
    });

    return basalPathGroups;
  };

  this.actual = data;

  this.data = data || [];
  if (this.data.length > 0) {
    this.endpoints = [this.data[0].normalTime, dt.addDuration(this.data[this.data.length - 1].normalTime, this.data[this.data.length - 1].duration)];
  }
}

module.exports = BasalUtil;
<commit_msg>Fix for automated delivery suspends<commit_after>/*
 * == BSD2 LICENSE ==
 * Copyright (c) 2014, Tidepool Project
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the associated License, which is identical to the BSD 2-Clause
 * License as published by the Open Source Initiative at opensource.org.
 *
 * This program 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 License for more details.
 *
 * You should have received a copy of the License along with this program; if
 * not, you can obtain one from Tidepool Project at tidepool.org.
 * == BSD2 LICENSE ==
 */

/* jshint esversion:6 */

var _ = require('lodash');

var format = require('./util/format');
var dt = require('./util/datetime');

var MS_IN_HOUR = 3600000;
var MS_IN_DAY = 86400000;

function BasalUtil(data) {

  this.scheduleTotal = function(schedule) {
    if ((! (schedule && Array.isArray(schedule) && schedule.length > 0))) {
      return NaN;
    }
    var total = 0, duration;
    schedule.push({
      start: MS_IN_DAY
    });
    for (var i = 1; i <= schedule.length - 1; i++) {
      duration = schedule[i].start - schedule[i - 1].start;
      total += this.segmentDose(duration, schedule[i - 1].rate);
    }

    schedule.pop();

    return format.fixFloatingPoint(total);
  };

  this.segmentDose = function(duration, rate) {
    var hours = duration / MS_IN_HOUR;
    return format.fixFloatingPoint(hours * rate);
  };

  this.subtotal = function(endpoints) {
    var dose = 0.0;
    var start = new Date(endpoints.start.datetime), end = new Date(endpoints.end.datetime);
    // handle first segment, which may have started before the start endpoint
    var segment = this.actual[endpoints.start.index];
    dose += this.segmentDose((new Date(segment.normalEnd) - start), segment.rate);
    var i = endpoints.start.index + 1;
    while (i < endpoints.end.index) {
      segment = this.actual[i];
      dose += this.segmentDose(segment.duration, segment.rate);
      i++;
    }
    segment = this.actual[endpoints.end.index];
    // handle last segment, which may go past the end endpoint
    dose += this.segmentDose((end - new Date(segment.normalTime)), segment.rate);
    return format.fixFloatingPoint(dose);
  };

  this.isContinuous = function(s, e) {
    var start = new Date(s), end = new Date(e);
    var startIndex = _.findIndex(this.actual, function(segment) {
        return (new Date(segment.normalTime).valueOf() <= start) && (start <= new Date(segment.normalEnd).valueOf());
      });
    var endIndex = _.findIndex(this.actual, function(segment) {
        return (new Date(segment.normalTime).valueOf() <= end) && (end <= new Date(segment.normalEnd).valueOf());
      });
    if ((startIndex >= 0) && (endIndex >= 0)) {
      var i = startIndex;
      while (i < endIndex) {
        var s1 = this.actual[i], s2 = this.actual[i + 1];
        if (s1.normalEnd !== s2.normalTime) {
          return false;
        }
        i++;
      }
      return {
        start: {
          datetime: start.toISOString(),
          index: startIndex
        },
        end: {
          datetime: end.toISOString(),
          index: endIndex
        }
      };
    }
    else {
      return false;
    }
  };

  this.totalBasal = function(s, e, opts) {
    opts = opts || {};
    if (dt.verifyEndpoints(s, e, this.endpoints)) {
      var endpoints;
      if (dt.isTwentyFourHours(s, e)) {
        endpoints = this.isContinuous(s, e);
        if (endpoints) {
          return {total: this.subtotal(endpoints)};
        }
        else {
          return {total: NaN};
        }
      }
      else if (dt.isLessThanTwentyFourHours(s, e)) {
        return {total: NaN};
      }
      else {
        var dose = 0.0;
        var excluded = [];
        var start = new Date(s), end = new Date(e);
        var n = dt.getNumDays(s, e);
        for (var j = 0; j < n; j++) {
          var dayStart = new Date(start);
          var dayEnd = new Date(dayStart);
          dayEnd.setUTCDate(dayEnd.getUTCDate() + 1);
          endpoints = this.isContinuous(dayStart.toISOString(), dayEnd.toISOString());
          if (endpoints && dt.isTwentyFourHours(dayStart.toISOString(), dayEnd.toISOString())) {
            if (isNaN(this.subtotal(endpoints))) {
              excluded.push(dayStart.toISOString());
            }
            else {
              dose += this.subtotal(endpoints);
            }
          }
          else {
            excluded.push(dayStart.toISOString());
          }
          start.setUTCDate(start.getUTCDate() + 1);
        }
        if (excluded.length <= opts.exclusionThreshold) {
          return {
            total: dose,
            excluded: excluded
          };
        }
        else {
          return {
            total: NaN,
            excluded: excluded
          };
        }
      }
    }
    else {
      return {total: NaN};
    }
  };

  /**
   * getBasalPathGroupType
   * @param {Object} basal - single basal datum
   * @return {String} the path group type
   */
  this.getBasalPathGroupType = function(datum) {
    var deliveryType = _.get(datum, 'deliveryType');
    var suppressedDeliveryType = _.get(datum, 'suppressed.deliveryType');
    return _.contains([deliveryType, suppressedDeliveryType], 'automated')
      // ? deliveryType === 'automated' ? 'automated' : 'automatedSuppressed'
      ? 'automated'
      : 'regular';
  };

  /**
   * getBasalPathGroups
   * @param {Array} basals - Array of preprocessed Tidepool basal objects
   * @return {Array} groups of alternating 'automated' and 'regular' datums
   */
  this.getBasalPathGroups = function(basals) {
    var basalPathGroups = [];
    var currentPathType;
    _.each(basals, datum => {
      var pathType = this.getBasalPathGroupType(datum);
      if (pathType !== currentPathType) {
        currentPathType = pathType;
        basalPathGroups.push([]);
      }
      _.last(basalPathGroups).push(datum);
    });

    return basalPathGroups;
  };

  this.actual = data;

  this.data = data || [];
  if (this.data.length > 0) {
    this.endpoints = [this.data[0].normalTime, dt.addDuration(this.data[this.data.length - 1].normalTime, this.data[this.data.length - 1].duration)];
  }
}

module.exports = BasalUtil;
<|endoftext|>


<commit_before>var card_width = 300;
var card_height = 186;
var half_card_width = card_width / 2;
var half_card_height = card_height / 2;

var next_outgoing_i = 0; /* the index of the next node that's outgoing */
var next_incoming_i = 0;

/* Which card is currently displayed in the middle of the screen? */
var current_card = null;

/* Height of rendered graph */
var graph_height = null;
var min_graph_height = 808;

// scrolling
var top_row_offset = 0;
var bottom_row_offset = 0;

var nodes = [];
var cards = [];
var relations = []; // relations are between nodes
var links = []; // links are between cards

var nodes = [];
var relations = [];

function indexOfNode(node_id) {
    for (i = 0; i < nodes.length; i++) {
        if (nodes[i].id === node_id) {
            return i;
        }
    }
    return -1; // not found
}

function indexOfRelation(relation_id) {
    for (i = 0; i < relations.length; i++) {
        if (relations[i].id === relation_id) {
            return i;
        }
    }
    return -1; // not found
}

function set_incoming_and_outgoing(relation) {
    if (get_other_id(relation) === null) {
        relation.from.outgoing.push(relation.to);
        relation.to.incoming.push(relation.from);
    }
}

function set_relation_id(relation, node) {
    if (relation.from === node.id) {
        relation.from = node;
    } else if (relation.to === node.id) {
        relation.to = node;
    } else {
        console.log("Uh oh, node " + node.id + " not found for relation " + relation.id);
    }

    set_incoming_and_outgoing(relation);
}

function get_other_id(relation) {
    // call this after calling "set_relation_id"
    if (typeof relation.from === 'number') {
        return relation.from;
    } else if (typeof relation.to === 'number') {
        return relation.to;
    } else {
        return null;
    }
}

function ajax_get_node(node_id, callback) {
    // if node already exists, DON'T evenn call this function!
    // mock ajax function for now
    $.ajax({
        url: "mock_ajax/" + node_id + ".json",
        type: "GET",
        dataType: "json",
    }).done(function (data) {
        data.incoming = [];
        data.outgoing = [];
        nodes.push(data);
        if (typeof callback !== 'undefined') {
            callback(data);
        }
    }).fail(function (jqXHR, textStatus, errorThrown) {
        console.log("Failed to get node " + node_id + ": " + errorThrown);
    });
}

function ajax_get_relation_nodes_function(new_relation, existing_node) {
    // http://stackoverflow.com/a/750506/257583
    return function () {
        if (indexOfRelation(new_relation.id) === -1) { // it's a new relation
            set_relation_id(new_relation, existing_node);
            var missing_id = get_other_id(new_relation);
            var missing_index = indexOfNode(missing_id);
            if (missing_index === -1) { // node is currently missing
                ajax_get_node(missing_id, function (new_node) {
                    set_relation_id(new_relation, new_node);
                    relations.push(new_relation);
                    draw_graph();
                })
            } else { // node already exists, just use it
                set_relation_id(new_relation, nodes[missing_index]);
                relations.push(new_relation);
            }
        }
    }
}

function ajax_get_relations_of(node, callback) {
    // get relations and make sure that nodes for relations get fetched as well
    // mock ajax function for now
    $.ajax({
        url: "mock_ajax/arg_" + node.id + "_relations.json",
        type: "GET",
        dataType: "json",
    }).done(function (data) {
        ajax_relation_nodes_functions = []
        for (var i = 0; i < data.length; i++) {
            ajax_relation_nodes_functions.push(ajax_get_relation_nodes_function(data[i], node));
        }
        for (var i = 0; i < ajax_relation_nodes_functions.length; i++) {
            ajax_relation_nodes_functions[i]();
        }
        if (typeof callback !== 'undefined') {
            callback(data);
        }
    }).fail(function (jqXHR, textStatus, errorThrown) {
        console.log("Failed to get relations of node " + node.id + ": " + errorThrown);
    });
}

function ajax_get_card(node) {
    ajax_get_relations_of(node, function (relations) {
        draw_graph();
    });
}

function is_outgoing(node) {
    return current_card.outgoing.indexOf(node) != -1;
}

function is_incoming(node) {
    return current_card.incoming.indexOf(node) != -1;
}

function node_visible(node) {
    return (node == current_card) || is_outgoing(node) || is_incoming(node);
}

function link_visible(link) {
    // both ends of a link must be visible for a link to be visible
    return node_visible(link.from) && node_visible(link.to);
}

function x_pos(node) {
    if (node == current_card) {
        node.i = 0; // doesn't matter for current_card
        return (window.innerWidth / 2) - half_card_width;
    } else if (is_outgoing(node)) {
        if (node.i == null) {
            node.i = next_outgoing_i;
            next_outgoing_i++;
        }
        return top_row_offset + (node.i * (card_width + 10) + 10);
    } else if (is_incoming(node)) {
        if (node.i == null) {
            node.i = next_incoming_i;
            next_incoming_i++;
        }
        return bottom_row_offset + (node.i * (card_width + 10) + 10);
    } else {
        if (node.previously_current) {
            return window.innerWidth + 10;
        } else if (node.previously_outgoing || node.previously_incoming) {
            return (window.innerWidth / 2) - half_card_width;
        }
    }
}

function y_pos(node) {
    if (node == current_card) {
        return (graph_height / 2) - half_card_height;
    } else if (is_outgoing(node)) {
        return 10;
    } else if (is_incoming(node)) {
        return graph_height - 10 - card_height;
    } else {
        if (node.previously_current) {
            return (graph_height / 2) - half_card_height;
        } else if (node.previously_outgoing) {
            return -10 - card_height;
        } else if (node.previously_incoming) {
            return graph_height + 10;
        } else {
            console.log(node);
        }
    }
}

var drag = d3.behavior.drag()
            .on("drag", function () {
                console.log("woo");
                if (d3.event.y <= (card_height + 20)) {
                    top_row_offset += d3.event.dx;
                    draw_graph(0);
                } else if (d3.event.y >= (graph_height - card_height - 20)) {
                    bottom_row_offset += d3.event.dx;
                    draw_graph(0);
                }
            })

function pos2str(pos) {
    return pos[0] + "," + pos[1] + " ";
}

function compute_link_bezier_curve(link) {
    var from = link.from;
    var to = link.to;
    var start_pos = [x_pos(from, from.i) + half_card_width, y_pos(from, from.i)];
    var end_pos = [x_pos(to, to.i) + half_card_width,
                    /* marker-widht is 3, stroke-width is 5 */
                   y_pos(to, to.i) + card_height + (5 * 3)];
    var height = end_pos[1] - start_pos[1];
    var control1 = [start_pos[0], start_pos[1] + (height / 2)];
    var control2 = [end_pos[0], end_pos[1] - (height / 2)];
    return "M" + pos2str(start_pos) + "C" + pos2str(control1)
            + pos2str(control2) + pos2str(end_pos);
}

function reset_globals() {
    for (var i=0; i < nodes.length; i++) {
        node = nodes[i];
        node.i = null;
    }
    next_outgoing_i = 0;
    next_incoming_i = 0;
}

function make_cards() {
    var svg = d3.select("svg#graph");
    cards = svg.selectAll("g").data(nodes);
    new_cards = cards.enter().append("g").classed("argument", true);
    new_cards.append("rect").attr("width", card_width).attr("height", card_height)
                .style("opacity", 0).call(drag);
    var switch_objects = new_cards.append("switch").call(drag);
    switch_objects.append("foreignObject").classed("foreign-object", true)
            .attr("requiredFeatures", "http://www.w3.org/TR/SVG11/feature#Extensibility")
            .attr("width", card_width).attr("height", card_height)
            .append("xhtml:div").classed("summary", true).append("p")
            .classed("summary", true).text(function (d) {
                return d.summary;
            }).attr("xmlns", "http://www.w3.org/1999/xhtml");
    switch_objects.append("text").attr("x", 100).attr("y", 100)
        .text("Sorry, your browser is not currently supported.");
    new_cards.on("click", function (d) {
        if (d3.event.defaultPrevented) return;

        top_row_offset = 0;
        bottom_row_offset = 0;

        current_card = d;
        ajax_get_card(d);
        draw_graph();
    })
}

function make_links() {
    var svg = d3.select("svg#graph");
    links = svg.selectAll("path").data(relations);
    links.enter().append("path")
        .attr("class", function(d) {
            return d.type;
        }).attr("marker-end", function(d) {
            return "url(#arrow-" + d.type + ")";
        }).style("opacity", 0);
}

function draw_graph(transition_time) {
    transition_time = typeof transition_time === 'undefined' ? 500 : transition_time;

    reset_globals();
    make_cards();
    make_links();

    var svg = d3.select("svg#graph");
    d3.selectAll("g rect").transition().duration(transition_time)
        .attr("x", x_pos).attr("y", y_pos).style("opacity", 1);
    // http://stackoverflow.com/a/11743721/257583
    // also, doesn't matter if the text is opaque or not since it's the
    // same color as the background
    d3.selectAll(".foreign-object").transition().duration(transition_time)
        .attr("x", x_pos).attr("y", y_pos);
    cards.each(function (d) {
        if (current_card == d) {
            d.previously_current = true;
            d.previously_outgoing = false;
            d.previously_incoming = false;
        } else if (is_outgoing(d)) {
            d.previously_current = false;
            d.previously_outgoing = true;
            d.previously_incoming = false;
        } else if (is_incoming(d)) {
            d.previously_current = false;
            d.previously_outgoing = false;
            d.previously_incoming = true;
        } // else shouldn't happen
    });
    links.transition().duration(transition_time).style("opacity", function (d) {
        // if one end of the link is currently displayed, then the other is too
        if (link_visible(d)) {
            return 1;
        } else {
            return 0;
        }
    }).attr("d", compute_link_bezier_curve);
}

window.onload = function () {
    if ($("#graph").height() < min_graph_height) {
        alert("Whoops, looks like some things won't be displaying correctly. Please tell us about this.");
    }
    graph_height = $("#graph").height();
    ajax_get_node(1, function (data) {
        current_card = data;
        draw_graph();

        ajax_get_relations_of(nodes[0], function (data) {
            draw_graph();
        })
    })

    draw_graph();
}<commit_msg>Prevent user from dragging cards past left edge<commit_after>var card_width = 300;
var card_height = 186;
var half_card_width = card_width / 2;
var half_card_height = card_height / 2;

var next_outgoing_i = 0; /* the index of the next node that's outgoing */
var next_incoming_i = 0;

/* Which card is currently displayed in the middle of the screen? */
var current_card = null;

/* Height of rendered graph */
var graph_height = null;
var min_graph_height = 808;

// scrolling
var top_row_offset = 0;
var bottom_row_offset = 0;

var nodes = [];
var cards = [];
var relations = []; // relations are between nodes
var links = []; // links are between cards

var nodes = [];
var relations = [];

function indexOfNode(node_id) {
    for (i = 0; i < nodes.length; i++) {
        if (nodes[i].id === node_id) {
            return i;
        }
    }
    return -1; // not found
}

function indexOfRelation(relation_id) {
    for (i = 0; i < relations.length; i++) {
        if (relations[i].id === relation_id) {
            return i;
        }
    }
    return -1; // not found
}

function set_incoming_and_outgoing(relation) {
    if (get_other_id(relation) === null) {
        relation.from.outgoing.push(relation.to);
        relation.to.incoming.push(relation.from);
    }
}

function set_relation_id(relation, node) {
    if (relation.from === node.id) {
        relation.from = node;
    } else if (relation.to === node.id) {
        relation.to = node;
    } else {
        console.log("Uh oh, node " + node.id + " not found for relation " + relation.id);
    }

    set_incoming_and_outgoing(relation);
}

function get_other_id(relation) {
    // call this after calling "set_relation_id"
    if (typeof relation.from === 'number') {
        return relation.from;
    } else if (typeof relation.to === 'number') {
        return relation.to;
    } else {
        return null;
    }
}

function ajax_get_node(node_id, callback) {
    // if node already exists, DON'T evenn call this function!
    // mock ajax function for now
    $.ajax({
        url: "mock_ajax/" + node_id + ".json",
        type: "GET",
        dataType: "json",
    }).done(function (data) {
        data.incoming = [];
        data.outgoing = [];
        nodes.push(data);
        if (typeof callback !== 'undefined') {
            callback(data);
        }
    }).fail(function (jqXHR, textStatus, errorThrown) {
        console.log("Failed to get node " + node_id + ": " + errorThrown);
    });
}

function ajax_get_relation_nodes_function(new_relation, existing_node) {
    // http://stackoverflow.com/a/750506/257583
    return function () {
        if (indexOfRelation(new_relation.id) === -1) { // it's a new relation
            set_relation_id(new_relation, existing_node);
            var missing_id = get_other_id(new_relation);
            var missing_index = indexOfNode(missing_id);
            if (missing_index === -1) { // node is currently missing
                ajax_get_node(missing_id, function (new_node) {
                    set_relation_id(new_relation, new_node);
                    relations.push(new_relation);
                    draw_graph();
                })
            } else { // node already exists, just use it
                set_relation_id(new_relation, nodes[missing_index]);
                relations.push(new_relation);
            }
        }
    }
}

function ajax_get_relations_of(node, callback) {
    // get relations and make sure that nodes for relations get fetched as well
    // mock ajax function for now
    $.ajax({
        url: "mock_ajax/arg_" + node.id + "_relations.json",
        type: "GET",
        dataType: "json",
    }).done(function (data) {
        ajax_relation_nodes_functions = []
        for (var i = 0; i < data.length; i++) {
            ajax_relation_nodes_functions.push(ajax_get_relation_nodes_function(data[i], node));
        }
        for (var i = 0; i < ajax_relation_nodes_functions.length; i++) {
            ajax_relation_nodes_functions[i]();
        }
        if (typeof callback !== 'undefined') {
            callback(data);
        }
    }).fail(function (jqXHR, textStatus, errorThrown) {
        console.log("Failed to get relations of node " + node.id + ": " + errorThrown);
    });
}

function ajax_get_card(node) {
    ajax_get_relations_of(node, function (relations) {
        draw_graph();
    });
}

function is_outgoing(node) {
    return current_card.outgoing.indexOf(node) != -1;
}

function is_incoming(node) {
    return current_card.incoming.indexOf(node) != -1;
}

function node_visible(node) {
    return (node == current_card) || is_outgoing(node) || is_incoming(node);
}

function link_visible(link) {
    // both ends of a link must be visible for a link to be visible
    return node_visible(link.from) && node_visible(link.to);
}

function x_pos(node) {
    if (node == current_card) {
        node.i = 0; // doesn't matter for current_card
        return (window.innerWidth / 2) - half_card_width;
    } else if (is_outgoing(node)) {
        if (node.i == null) {
            node.i = next_outgoing_i;
            next_outgoing_i++;
        }
        return top_row_offset + (node.i * (card_width + 10) + 10);
    } else if (is_incoming(node)) {
        if (node.i == null) {
            node.i = next_incoming_i;
            next_incoming_i++;
        }
        return bottom_row_offset + (node.i * (card_width + 10) + 10);
    } else {
        if (node.previously_current) {
            return window.innerWidth + 10;
        } else if (node.previously_outgoing || node.previously_incoming) {
            return (window.innerWidth / 2) - half_card_width;
        }
    }
}

function y_pos(node) {
    if (node == current_card) {
        return (graph_height / 2) - half_card_height;
    } else if (is_outgoing(node)) {
        return 10;
    } else if (is_incoming(node)) {
        return graph_height - 10 - card_height;
    } else {
        if (node.previously_current) {
            return (graph_height / 2) - half_card_height;
        } else if (node.previously_outgoing) {
            return -10 - card_height;
        } else if (node.previously_incoming) {
            return graph_height + 10;
        } else {
            console.log(node);
        }
    }
}

function max_top_row_offset() {
    return Math.min(0, window.innerWidth - (10 + next_outgoing_i * (card_width + 10)));
}

function max_bottom_row_offset() {
    return Math.min(0, window.innerWidth - (10 + next_incoming_i * (card_width + 10)));
}

var drag = d3.behavior.drag()
            .on("drag", function () {
                console.log("woo");
                if (d3.event.y <= (card_height + 20)) {
                    top_row_offset += d3.event.dx;
                    draw_graph(0);

                    if ((top_row_offset < 0) && (top_row_offset < max_top_row_offset())) {
                        top_row_offset = max_top_row_offset();
                        draw_graph(0);
                    }
                } else if (d3.event.y >= (graph_height - card_height - 20)) {
                    bottom_row_offset += d3.event.dx;
                    draw_graph(0);

                    if ((bottom_row_offset < 0) && (bottom_row_offset < max_bottom_row_offset())) {
                        bottom_row_offset = max_bottom_row_offset();
                        draw_graph(0);
                    }
                }
            })

function pos2str(pos) {
    return pos[0] + "," + pos[1] + " ";
}

function compute_link_bezier_curve(link) {
    var from = link.from;
    var to = link.to;
    var start_pos = [x_pos(from, from.i) + half_card_width, y_pos(from, from.i)];
    var end_pos = [x_pos(to, to.i) + half_card_width,
                    /* marker-widht is 3, stroke-width is 5 */
                   y_pos(to, to.i) + card_height + (5 * 3)];
    var height = end_pos[1] - start_pos[1];
    var control1 = [start_pos[0], start_pos[1] + (height / 2)];
    var control2 = [end_pos[0], end_pos[1] - (height / 2)];
    return "M" + pos2str(start_pos) + "C" + pos2str(control1)
            + pos2str(control2) + pos2str(end_pos);
}

function reset_globals() {
    for (var i=0; i < nodes.length; i++) {
        node = nodes[i];
        node.i = null;
    }
    next_outgoing_i = 0;
    next_incoming_i = 0;
}

function make_cards() {
    var svg = d3.select("svg#graph");
    cards = svg.selectAll("g").data(nodes);
    new_cards = cards.enter().append("g").classed("argument", true);
    new_cards.append("rect").attr("width", card_width).attr("height", card_height)
                .style("opacity", 0).call(drag);
    var switch_objects = new_cards.append("switch").call(drag);
    switch_objects.append("foreignObject").classed("foreign-object", true)
            .attr("requiredFeatures", "http://www.w3.org/TR/SVG11/feature#Extensibility")
            .attr("width", card_width).attr("height", card_height)
            .append("xhtml:div").classed("summary", true).append("p")
            .classed("summary", true).text(function (d) {
                return d.summary;
            }).attr("xmlns", "http://www.w3.org/1999/xhtml");
    switch_objects.append("text").attr("x", 100).attr("y", 100)
        .text("Sorry, your browser is not currently supported.");
    new_cards.on("click", function (d) {
        if (d3.event.defaultPrevented) return;

        top_row_offset = 0;
        bottom_row_offset = 0;

        current_card = d;
        ajax_get_card(d);
        draw_graph();
    })
}

function make_links() {
    var svg = d3.select("svg#graph");
    links = svg.selectAll("path").data(relations);
    links.enter().append("path")
        .attr("class", function(d) {
            return d.type;
        }).attr("marker-end", function(d) {
            return "url(#arrow-" + d.type + ")";
        }).style("opacity", 0);
}

function draw_graph(transition_time) {
    transition_time = typeof transition_time === 'undefined' ? 500 : transition_time;

    reset_globals();
    make_cards();
    make_links();

    var svg = d3.select("svg#graph");
    d3.selectAll("g rect").transition().duration(transition_time)
        .attr("x", x_pos).attr("y", y_pos).style("opacity", 1);
    // http://stackoverflow.com/a/11743721/257583
    // also, doesn't matter if the text is opaque or not since it's the
    // same color as the background
    d3.selectAll(".foreign-object").transition().duration(transition_time)
        .attr("x", x_pos).attr("y", y_pos);
    cards.each(function (d) {
        if (current_card == d) {
            d.previously_current = true;
            d.previously_outgoing = false;
            d.previously_incoming = false;
        } else if (is_outgoing(d)) {
            d.previously_current = false;
            d.previously_outgoing = true;
            d.previously_incoming = false;
        } else if (is_incoming(d)) {
            d.previously_current = false;
            d.previously_outgoing = false;
            d.previously_incoming = true;
        } // else shouldn't happen
    });
    links.transition().duration(transition_time).style("opacity", function (d) {
        // if one end of the link is currently displayed, then the other is too
        if (link_visible(d)) {
            return 1;
        } else {
            return 0;
        }
    }).attr("d", compute_link_bezier_curve);
}

window.onload = function () {
    if ($("#graph").height() < min_graph_height) {
        alert("Whoops, looks like some things won't be displaying correctly. Please tell us about this.");
    }
    graph_height = $("#graph").height();
    ajax_get_node(1, function (data) {
        current_card = data;
        draw_graph();

        ajax_get_relations_of(nodes[0], function (data) {
            draw_graph();
        })
    })

    draw_graph();
}<|endoftext|>


<commit_before>import Effect, { ConfigUI, fract } from './effect';
import { parseHtml } from '../ui/util';
import { ImageCapture } from 'image-capture/lib/imagecapture';

const EffectName = 'Webcam';
const EffectDescription = 'Make use of the user\'s webcam as the particles\' color values';

class WebcamConfigUI extends ConfigUI {
  constructor() {
    super();
    this.element = parseHtml(`
      <fieldset>
        <legend>${EffectName}</legend>
        Nothing to be configured :)
      </fieldset>
    `);
    const ui = this.element;
  }

  getElement() {
    return this.element;
  }

  getConfig() {
    return {};
  }

  applyConfig(config) {
  }
}

export default class WebcamEffect extends Effect {
  static registerAsync(instance, props) {
    // State variables
    const canvas = document.createElement('canvas');
    let stream = null;
    let stopped = false;
    const stop = () => { stopped = true; };
    const isActive = () => {
      const clock = props.clock;
      const time = clock.getTime();
      return !clock.isPaused() && instance.timeBegin <= time && time <= instance.timeEnd;
    };

    // Shutdown hook
    props.state.addHook(() => {
      stop();
      // FIXME understand and document when this can happen.
      // E.g. when the getUserMedia() request is ignored in icognito
      // mode
      if (stream !== null) {
        const allTracks = stream.getTracks();
        for (let i = 0; i < allTracks.length; i++) {
          allTracks[i].stop();
        }
      }
    });

    const mediaConstraints = {
      audio: false,
      video: true // we want video
    };
    // Let's ask the browser if we can haz video
    return navigator.mediaDevices.getUserMedia(mediaConstraints)
    .then((theStream) => {
      stream = theStream;
      const videoTracks = stream.getVideoTracks();
      if (videoTracks.length === 0) {
        return Promise.reject('No video tracks in user media');
      }
      // We got a video feed!
      // Let's try to adapt it to our needs a little bit more
      const videoTrack = videoTracks[0];
      const constraints = {
        width: props.state.getWidth(),
        height: props.state.getHeight(),
        aspectRatio: props.state.getWidth() / props.state.getHeight(),
        facingMode: 'user'
      };
      // According to MDN, this shouldn't ever reject.
      // TODO maybe add an assertion for that
      return videoTrack.applyConstraints(constraints)
        .then(() => Promise.resolve(videoTrack), (err) => Promise.reject(err));
    }, (err) => Promise.reject(err))
    .then((videoTrack) => {
      // Now this is where the magic happens
      const capture = new ImageCapture(videoTrack);
      const retried = false;
      const grabLoop = (imageOrTimestamp) => {
        if (isActive() && imageOrTimestamp && (typeof imageOrTimestamp) !== 'number') {
          const image = imageOrTimestamp;
          const w = image.width;
          const h = image.height;
          // FIXME the camera resolution shouldn't change all that often
          //       Maybe we can do this only once. Or we keep relying on
          //       the browser to optimize.
          canvas.width = w;
          canvas.height = h;
          const ctx = canvas.getContext('2d');
          ctx.scale(-1, -1);
          ctx.drawImage(image, 0, 0, -w, -h);
          const pd = props.state.createParticleData(canvas, 'fit-image', {x: 'crop-both', y: 'crop-both'});
          props.state.setParticleData(pd);
        }
        if (!stopped) {
          // FIXME if we don't grab frames, Chrome will soon make the
          // track invalid, causing the next grabFrame to throw an error
          if (true || isActive()) {
            capture.grabFrame()
            .then(grabLoop, (err) => {
              // FIXME Firefox needs some time to get ready for grabbing
              // frames, so let's try this one more time if the first
              // one didn't succeed
              if (!retried) {
                retried = true;
                window.setTimeout(() => grabLoop(0), 200);
              } else {
                return Promise.reject(err);
              }
            });
          } else {
            window.requestAnimationFrame(grabLoop);
          }
        }
      };
      // start grabbing images!
      window.requestAnimationFrame(grabLoop);
    }, (err) => Promise.reject(err));
  }

  static getDisplayName() {
    return EffectName;
  }

  static getDescription() {
    return EffectDescription;
  }

  static getConfigUI() {
    if (!this._configUI) {
      this._configUI = new WebcamConfigUI();
    }

    return this._configUI;
  }

  static getDefaultConfig() {
    return {};
  }

  static getRandomConfig() {
    return {};
  }
}
<commit_msg>Fix stupid const-reassignment mistake<commit_after>import Effect, { ConfigUI, fract } from './effect';
import { parseHtml } from '../ui/util';
import { ImageCapture } from 'image-capture/lib/imagecapture';

const EffectName = 'Webcam';
const EffectDescription = 'Make use of the user\'s webcam as the particles\' color values';

class WebcamConfigUI extends ConfigUI {
  constructor() {
    super();
    this.element = parseHtml(`
      <fieldset>
        <legend>${EffectName}</legend>
        Nothing to be configured :)
      </fieldset>
    `);
    const ui = this.element;
  }

  getElement() {
    return this.element;
  }

  getConfig() {
    return {};
  }

  applyConfig(config) {
  }
}

export default class WebcamEffect extends Effect {
  static registerAsync(instance, props) {
    // State variables
    const canvas = document.createElement('canvas');
    let stream = null;
    let stopped = false;
    const stop = () => { stopped = true; };
    const isActive = () => {
      const clock = props.clock;
      const time = clock.getTime();
      return !clock.isPaused() && instance.timeBegin <= time && time <= instance.timeEnd;
    };

    // Shutdown hook
    props.state.addHook(() => {
      stop();
      // FIXME understand and document when this can happen.
      // E.g. when the getUserMedia() request is ignored in icognito
      // mode
      if (stream !== null) {
        const allTracks = stream.getTracks();
        for (let i = 0; i < allTracks.length; i++) {
          allTracks[i].stop();
        }
      }
    });

    const mediaConstraints = {
      audio: false,
      video: true // we want video
    };
    // Let's ask the browser if we can haz video
    return navigator.mediaDevices.getUserMedia(mediaConstraints)
    .then((theStream) => {
      stream = theStream;
      const videoTracks = stream.getVideoTracks();
      if (videoTracks.length === 0) {
        return Promise.reject('No video tracks in user media');
      }
      // We got a video feed!
      // Let's try to adapt it to our needs a little bit more
      const videoTrack = videoTracks[0];
      const constraints = {
        width: props.state.getWidth(),
        height: props.state.getHeight(),
        aspectRatio: props.state.getWidth() / props.state.getHeight(),
        facingMode: 'user'
      };
      // According to MDN, this shouldn't ever reject.
      // TODO maybe add an assertion for that
      return videoTrack.applyConstraints(constraints)
        .then(() => Promise.resolve(videoTrack), (err) => Promise.reject(err));
    }, (err) => Promise.reject(err))
    .then((videoTrack) => {
      // Now this is where the magic happens
      const capture = new ImageCapture(videoTrack);
      let retried = false;
      const grabLoop = (imageOrTimestamp) => {
        if (isActive() && imageOrTimestamp && (typeof imageOrTimestamp) !== 'number') {
          const image = imageOrTimestamp;
          const w = image.width;
          const h = image.height;
          // FIXME the camera resolution shouldn't change all that often
          //       Maybe we can do this only once. Or we keep relying on
          //       the browser to optimize.
          canvas.width = w;
          canvas.height = h;
          const ctx = canvas.getContext('2d');
          ctx.scale(-1, -1);
          ctx.drawImage(image, 0, 0, -w, -h);
          const pd = props.state.createParticleData(canvas, 'fit-image', {x: 'crop-both', y: 'crop-both'});
          props.state.setParticleData(pd);
        }
        if (!stopped) {
          // FIXME if we don't grab frames, Chrome will soon make the
          // track invalid, causing the next grabFrame to throw an error
          if (true || isActive()) {
            capture.grabFrame()
            .then(grabLoop, (err) => {
              // FIXME Firefox needs some time to get ready for grabbing
              // frames, so let's try this one more time if the first
              // one didn't succeed
              if (!retried) {
                retried = true;
                window.setTimeout(() => grabLoop(0), 200);
              } else {
                return Promise.reject(err);
              }
            });
          } else {
            window.requestAnimationFrame(grabLoop);
          }
        }
      };
      // start grabbing images!
      window.requestAnimationFrame(grabLoop);
    }, (err) => Promise.reject(err));
  }

  static getDisplayName() {
    return EffectName;
  }

  static getDescription() {
    return EffectDescription;
  }

  static getConfigUI() {
    if (!this._configUI) {
      this._configUI = new WebcamConfigUI();
    }

    return this._configUI;
  }

  static getDefaultConfig() {
    return {};
  }

  static getRandomConfig() {
    return {};
  }
}
<|endoftext|>


<commit_before>/*!
 * jquery-confirm v1.7.0 (http://craftpip.github.io/jquery-confirm/)
 * Author: Boniface Pereira
 * Website: www.craftpip.com
 * Contact: hey@craftpip.com
 *
 * Copyright 2013-2015 jquery-confirm
 * Licensed under MIT (https://github.com/craftpip/jquery-confirm/blob/master/LICENSE)
 */

if (typeof jQuery === 'undefined') {
    throw new Error('jquery-confirm requires jQuery');
}

var jconfirm, Jconfirm;
(function ($) {
    "use strict";
    $.confirm = function (options) {
        /*
         *  Alias of jconfirm
         */
        return jconfirm(options);
    };
    $.alert = function (options) {
        /*
         *  Alias of jconfirm
         */
        options.cancelButton = false;
        return jconfirm(options);
    };
    $.dialog = function (options) {
        /*
         *  Alias of jconfirm
         */
        options.cancelButton = false;
        options.confirmButton = false;
        return jconfirm(options);
    };
    jconfirm = function (options) {
        /*
         * initial function for calling.
         */
        if (jconfirm.defaults) {
            /*
             * Merge global defaults with plugin defaults
             */
            $.extend(jconfirm.pluginDefaults, jconfirm.defaults);
        }
        /*
         * merge options with plugin defaults.
         */
        var options = $.extend({}, jconfirm.pluginDefaults, options);
        return new Jconfirm(options);
    };
    Jconfirm = function (options) {
        /*
         * constructor function Jconfirm,
         * options = user options.
         */
        $.extend(this, options);
        this._init();
    };
    Jconfirm.prototype = {
        _init: function () {
            var that = this;
            this._rand = Math.round(Math.random() * 99999);
            this._buildHTML();
            this._bindEvents();
            setTimeout(function () {
                that.open();
            }, 0);
        },
        animations: ['anim-scale', 'anim-top', 'anim-bottom', 'anim-left', 'anim-right', 'anim-zoom', 'anim-opacity', 'anim-none', 'anim-rotate', 'anim-rotatex', 'anim-rotatey', 'anim-scalex', 'anim-scaley'],
        _buildHTML: function () {
            var that = this;

            /*
             * Cleaning animations.
             */
            this.animation = 'anim-' + this.animation.toLowerCase();
            if (this.animation === 'none')
                this.animationSpeed = 0;

            /*
             * Append html to body.
             */
            this.$el = $(this.template).appendTo(this.container).addClass(this.theme);
            this.$el.find('.jconfirm-box-container').addClass(this.columnClass);

            this.CSS = {
                '-webkit-transition-duration': this.animationSpeed / 1000 + 's',
                'transition-duration': this.animationSpeed / 1000 + 's',
                '-webkjit-transition-timing-function': 'cubic-bezier(0.27, 1.12, 0.32, ' + this.animationBounce + ')',
                'transition-timing-function': 'cubic-bezier(0.27, 1.12, 0.32, ' + this.animationBounce + ')',
            };

            this.$el.find('.jconfirm-bg').css(this.CSS);
            this.$b = this.$el.find('.jconfirm-box').css(this.CSS).addClass(this.animation);

            /*
             * Setup title contents
             */
            this.setTitle();
            this.contentDiv = this.$el.find('div.content');

            /*
             * Settings up buttons
             */
            this.$btnc = this.$el.find('.buttons');
            if (this.confirmButton && this.confirmButton.trim() !== '') {
                this.$confirmButton = $('<button class="btn">' + this.confirmButton + '</button>')
                        .appendTo(this.$btnc)
                        .addClass(this.confirmButtonClass);
            }
            if (this.cancelButton && this.cancelButton.trim() !== '') {
                this.$cancelButton = $('<button class="btn">' + this.cancelButton + '</button>')
                        .appendTo(this.$btnc)
                        .addClass(this.cancelButtonClass);
            }

            if (!this.confirmButton && !this.cancelButton) {
                this.$btnc.remove();
            }

            if(!this.confirmButton && !this.cancelButton && this.closeIcon == null){
                this.$closeButton = this.$b.find('.closeIcon').show();
            }

            if (this.closeIcon === true){
                this.$closeButton = this.$b.find('.closeIcon').show();
            }

            this.setContent();

            if (this.autoClose)
                this._startCountDown();
        },
        setTitle: function(string){
            this.title = (typeof string !== 'undefined') ? string : this.title;

            if (this.title) {
                this.$el.find('div.title').html(' ' + this.title);
            } else {
                this.$el.find('div.title').remove();
            }
        },
        setContent: function (string) {
            var that = this;

            /*
             * Set content.
             */
            if (typeof string !== undefined && typeof string === 'string') {
                this.content = string;
                this.setContent();
            } else if (typeof this.content === 'boolean') {
                if (!this.content)
                    this.contentDiv.remove();
                else
                    console.error('Invalid option for property content: passed TRUE');
            } else if (typeof this.content === 'string') {

                if (this.content.substr(0, 4).toLowerCase() === 'url:') {
                    this.contentDiv.html('');
                    this.$btnc.find('button').prop('disabled', true);
                    var url = this.content.substring(4, this.content.length);
                    $.get(url).done(function (html) {
                        that.contentDiv.html(html);
                    }).always(function (data, status, xhr) {

                        if (typeof that.contentLoaded === 'function')
                            that.contentLoaded(data, status, xhr);

                        that.$btnc.find('button').prop('disabled', false);
                        that.setDialogCenter();
                    });
                } else {
                    this.contentDiv.html(this.content);
                }

            } else if (typeof this.content === 'function') {

                this.contentDiv.html('');
                this.$btnc.find('button').attr('disabled', 'disabled');

                var promise = this.content(this);
                if (typeof promise !== 'object') {
                    console.error('The content function must return jquery promise.');
                } else if (typeof promise.always !== 'function') {
                    console.error('The object returned is not a jquery promise.');
                } else {
                    promise.always(function () {
                        that.$btnc.find('button').removeAttr('disabled');
                        that.setDialogCenter();
                    });
                }

            } else {
                console.error('Invalid option for property content, passed: ' + typeof this.content);
            }

            this.setDialogCenter();
        },
        _startCountDown: function () {
            var opt = this.autoClose.split('|');
            if (/cancel/.test(opt[0]) && this.type === 'alert') {
                return false;
            } else if (/confirm|cancel/.test(opt[0])) {
                this.$cd = $('').appendTo(this['$' + opt[0] + 'Button']);
                var that = this;
                that.$cd.parent().click();
                var time = opt[1] / 1000;
                this.interval = setInterval(function () {
                    that.$cd.html(' [' + (time -= 1) + ']');
                    if (time === 0) {
                        that.$cd.parent().trigger('click');
                        clearInterval(that.interval);
                    }
                }, 1000);
            } else {
                console.error('Invalid option ' + opt[0] + ', must be confirm/cancel');
            }
        },
        _bindEvents: function () {
            var that = this;

            this.$el.find('.jconfirm-scrollpane').click(function (e) {
                if (that.backgroundDismiss) {
                    that.cancel();
                    that.close();
                } else {
                    that.$b.addClass('hilight');
                    setTimeout(function () {
                        that.$b.removeClass('hilight');
                    }, 400);
                }
            });

            this.$el.find('.jconfirm-box').click(function (e) {
                e.stopPropagation();
            });

            if (this.$confirmButton) {
                this.$confirmButton.click(function (e) {
                    e.preventDefault();
                    var r = that.confirm(that.$b);
                    if (typeof r === 'undefined' || r)
                        that.close();
                });
            }
            if (this.$cancelButton) {
                this.$cancelButton.click(function (e) {
                    e.preventDefault();
                    var r = that.cancel(that.$b);
                    if (typeof r === 'undefined' || r)
                        that.close();
                });
            }
            if (this.$closeButton) {
                this.$closeButton.click(function (e) {
                    e.preventDefault();
                    that.cancel();
                    that.close();
                });
            }
            if (this.keyboardEnabled) {
                setTimeout(function () {
                    $(window).on('keyup.' + this._rand, function (e) {
                        that.reactOnKey(e);
                    });
                }, 500);
            }

            $(window).on('resize.' + this._rand, function () {
                that.setDialogCenter(true);
            });

        },
        reactOnKey: function key(e) {

            /*
             * prevent keyup event if the dialog is not last!
             */
            var a = $('.jconfirm');
            if (a.eq(a.length - 1)[0] !== this.$el[0])
                return false;

            var key = e.which;
            if (key === 27) {
                /*
                 * if ESC key
                 */
                if (!this.backgroundDismiss) {
                    /*
                     * If background dismiss is false, Glow the modal.
                     */
                    this.$el.find('.jconfirm-bg').click();
                    return false;
                }

                if (this.$cancelButton) {
                    this.$cancelButton.click();
                } else {
                    this.close();
                }
            }
            if (key === 13 || key == 32) {
                /*
                 * if ENTER or SPACE key
                 */
                if (this.$confirmButton) {
                    this.$confirmButton.click();
                } else {

                }
            }
        },
        setDialogCenter: function (animate) {
            var windowHeight = $(window).height();
            var boxHeight = this.$b.outerHeight();
            var topMargin = (windowHeight - boxHeight) / 2;
            var minMargin = 100;
            if (boxHeight > (windowHeight - minMargin)) {
                var style = {
                    'margin-top': minMargin / 2,
                    'margin-bottom': minMargin / 2,
                }
            } else {
                var style = {
                    'margin-top': topMargin,
                }
            }
            if (animate) {
                this.$b.animate(style, {
                    duration: (animate) ? this.animationSpeed : 0,
                    queue: false
                });
            } else {
                this.$b.css(style);
            }
        },
        close: function () {
            var that = this;

            /*
             unbind the window resize & keyup event.
             */
            $(window).unbind('resize.' + this._rand);
            if (this.keyboardEnabled)
                $(window).unbind('keyup.' + this._rand);

            that.$el.find('.jconfirm-bg').removeClass('seen');

            this.$b.addClass(this.animation);
            $('body').removeClass('jconfirm-noscroll');

            setTimeout(function () {
                that.$el.remove();
            }, this.animationSpeed + 30); // wait 30 miliseconds more, ensure everything is done.

            jconfirm.record.closed += 1;
            jconfirm.record.currentlyOpen -= 1;
        },
        open: function () {
            var that = this;
            if (this.isClosed())
                return false;

            that.$el.find('.jconfirm-bg').addClass('seen');

            $('body').addClass('jconfirm-noscroll');
            this.$b.removeClass(this.animations.join(' '));
            /**
             * Blur the focused elements, prevents re-execution with button press.
             */
            $('body :focus').trigger('blur');
            this.$b.find('input[autofocus]:visible:first').focus();
            jconfirm.record.opened += 1;
            jconfirm.record.currentlyOpen += 1;
            return true;
        },
        isClosed: function () {
            return (this.$el.css('display') === '') ? true : false;
        }
    };

    jconfirm.pluginDefaults = {
        template: '

',
        title: 'Hello',
        content: 'Are you sure to continue?',
        contentLoaded: function () {
        },
        icon: '',
        confirmButton: 'Okay',
        cancelButton: 'Cancel',
        confirmButtonClass: 'btn-default',
        cancelButtonClass: 'btn-default',
        theme: 'white',
        animation: 'scale',
        animationSpeed: 400,
        animationBounce: 1.5,
        keyboardEnabled: false,
        container: 'body',
        confirm: function () {
        },
        cancel: function () {
        },
        backgroundDismiss: true,
        autoClose: false,
        closeIcon: null,
        columnClass: 'col-md-6 col-md-offset-3',
    };

    jconfirm.record = {
        opened: 0,
        closed: 0,
        currentlyOpen: 0,
    };
})(jQuery);
<commit_msg>browser overflow scrollbar shown only if all jconfirm are closed.<commit_after>/*!
 * jquery-confirm v1.7.0 (http://craftpip.github.io/jquery-confirm/)
 * Author: Boniface Pereira
 * Website: www.craftpip.com
 * Contact: hey@craftpip.com
 *
 * Copyright 2013-2015 jquery-confirm
 * Licensed under MIT (https://github.com/craftpip/jquery-confirm/blob/master/LICENSE)
 */

if (typeof jQuery === 'undefined') {
    throw new Error('jquery-confirm requires jQuery');
}

var jconfirm, Jconfirm;
(function ($) {
    "use strict";
    $.confirm = function (options) {
        /*
         *  Alias of jconfirm
         */
        return jconfirm(options);
    };
    $.alert = function (options) {
        /*
         *  Alias of jconfirm
         */
        options.cancelButton = false;
        return jconfirm(options);
    };
    $.dialog = function (options) {
        /*
         *  Alias of jconfirm
         */
        options.cancelButton = false;
        options.confirmButton = false;
        return jconfirm(options);
    };
    jconfirm = function (options) {
        /*
         * initial function for calling.
         */
        if (jconfirm.defaults) {
            /*
             * Merge global defaults with plugin defaults
             */
            $.extend(jconfirm.pluginDefaults, jconfirm.defaults);
        }
        /*
         * merge options with plugin defaults.
         */
        var options = $.extend({}, jconfirm.pluginDefaults, options);
        return new Jconfirm(options);
    };
    Jconfirm = function (options) {
        /*
         * constructor function Jconfirm,
         * options = user options.
         */
        $.extend(this, options);
        this._init();
    };
    Jconfirm.prototype = {
        _init: function () {
            var that = this;
            this._rand = Math.round(Math.random() * 99999);
            this._buildHTML();
            this._bindEvents();
            setTimeout(function () {
                that.open();
            }, 0);
        },
        animations: ['anim-scale', 'anim-top', 'anim-bottom', 'anim-left', 'anim-right', 'anim-zoom', 'anim-opacity', 'anim-none', 'anim-rotate', 'anim-rotatex', 'anim-rotatey', 'anim-scalex', 'anim-scaley'],
        _buildHTML: function () {
            var that = this;

            /*
             * Cleaning animations.
             */
            this.animation = 'anim-' + this.animation.toLowerCase();
            if (this.animation === 'none')
                this.animationSpeed = 0;

            /*
             * Append html to body.
             */
            this.$el = $(this.template).appendTo(this.container).addClass(this.theme);
            this.$el.find('.jconfirm-box-container').addClass(this.columnClass);

            this.CSS = {
                '-webkit-transition-duration': this.animationSpeed / 1000 + 's',
                'transition-duration': this.animationSpeed / 1000 + 's',
                '-webkjit-transition-timing-function': 'cubic-bezier(0.27, 1.12, 0.32, ' + this.animationBounce + ')',
                'transition-timing-function': 'cubic-bezier(0.27, 1.12, 0.32, ' + this.animationBounce + ')',
            };

            this.$el.find('.jconfirm-bg').css(this.CSS);
            this.$b = this.$el.find('.jconfirm-box').css(this.CSS).addClass(this.animation);

            /*
             * Setup title contents
             */
            this.setTitle();
            this.contentDiv = this.$el.find('div.content');

            /*
             * Settings up buttons
             */
            this.$btnc = this.$el.find('.buttons');
            if (this.confirmButton && this.confirmButton.trim() !== '') {
                this.$confirmButton = $('<button class="btn">' + this.confirmButton + '</button>')
                        .appendTo(this.$btnc)
                        .addClass(this.confirmButtonClass);
            }
            if (this.cancelButton && this.cancelButton.trim() !== '') {
                this.$cancelButton = $('<button class="btn">' + this.cancelButton + '</button>')
                        .appendTo(this.$btnc)
                        .addClass(this.cancelButtonClass);
            }

            if (!this.confirmButton && !this.cancelButton) {
                this.$btnc.remove();
            }

            if(!this.confirmButton && !this.cancelButton && this.closeIcon == null){
                this.$closeButton = this.$b.find('.closeIcon').show();
            }

            if (this.closeIcon === true){
                this.$closeButton = this.$b.find('.closeIcon').show();
            }

            this.setContent();

            if (this.autoClose)
                this._startCountDown();
        },
        setTitle: function(string){
            this.title = (typeof string !== 'undefined') ? string : this.title;

            if (this.title) {
                this.$el.find('div.title').html(' ' + this.title);
            } else {
                this.$el.find('div.title').remove();
            }
        },
        setContent: function (string) {
            var that = this;

            /*
             * Set content.
             */
            if (typeof string !== undefined && typeof string === 'string') {
                this.content = string;
                this.setContent();
            } else if (typeof this.content === 'boolean') {
                if (!this.content)
                    this.contentDiv.remove();
                else
                    console.error('Invalid option for property content: passed TRUE');
            } else if (typeof this.content === 'string') {

                if (this.content.substr(0, 4).toLowerCase() === 'url:') {
                    this.contentDiv.html('');
                    this.$btnc.find('button').prop('disabled', true);
                    var url = this.content.substring(4, this.content.length);
                    $.get(url).done(function (html) {
                        that.contentDiv.html(html);
                    }).always(function (data, status, xhr) {

                        if (typeof that.contentLoaded === 'function')
                            that.contentLoaded(data, status, xhr);

                        that.$btnc.find('button').prop('disabled', false);
                        that.setDialogCenter();
                    });
                } else {
                    this.contentDiv.html(this.content);
                }

            } else if (typeof this.content === 'function') {

                this.contentDiv.html('');
                this.$btnc.find('button').attr('disabled', 'disabled');

                var promise = this.content(this);
                if (typeof promise !== 'object') {
                    console.error('The content function must return jquery promise.');
                } else if (typeof promise.always !== 'function') {
                    console.error('The object returned is not a jquery promise.');
                } else {
                    promise.always(function () {
                        that.$btnc.find('button').removeAttr('disabled');
                        that.setDialogCenter();
                    });
                }

            } else {
                console.error('Invalid option for property content, passed: ' + typeof this.content);
            }

            this.setDialogCenter();
        },
        _startCountDown: function () {
            var opt = this.autoClose.split('|');
            if (/cancel/.test(opt[0]) && this.type === 'alert') {
                return false;
            } else if (/confirm|cancel/.test(opt[0])) {
                this.$cd = $('').appendTo(this['$' + opt[0] + 'Button']);
                var that = this;
                that.$cd.parent().click();
                var time = opt[1] / 1000;
                this.interval = setInterval(function () {
                    that.$cd.html(' [' + (time -= 1) + ']');
                    if (time === 0) {
                        that.$cd.parent().trigger('click');
                        clearInterval(that.interval);
                    }
                }, 1000);
            } else {
                console.error('Invalid option ' + opt[0] + ', must be confirm/cancel');
            }
        },
        _bindEvents: function () {
            var that = this;

            this.$el.find('.jconfirm-scrollpane').click(function (e) {
                if (that.backgroundDismiss) {
                    that.cancel();
                    that.close();
                } else {
                    that.$b.addClass('hilight');
                    setTimeout(function () {
                        that.$b.removeClass('hilight');
                    }, 400);
                }
            });

            this.$el.find('.jconfirm-box').click(function (e) {
                e.stopPropagation();
            });

            if (this.$confirmButton) {
                this.$confirmButton.click(function (e) {
                    e.preventDefault();
                    var r = that.confirm(that.$b);
                    if (typeof r === 'undefined' || r)
                        that.close();
                });
            }
            if (this.$cancelButton) {
                this.$cancelButton.click(function (e) {
                    e.preventDefault();
                    var r = that.cancel(that.$b);
                    if (typeof r === 'undefined' || r)
                        that.close();
                });
            }
            if (this.$closeButton) {
                this.$closeButton.click(function (e) {
                    e.preventDefault();
                    that.cancel();
                    that.close();
                });
            }
            if (this.keyboardEnabled) {
                setTimeout(function () {
                    $(window).on('keyup.' + this._rand, function (e) {
                        that.reactOnKey(e);
                    });
                }, 500);
            }

            $(window).on('resize.' + this._rand, function () {
                that.setDialogCenter(true);
            });

        },
        reactOnKey: function key(e) {

            /*
             * prevent keyup event if the dialog is not last!
             */
            var a = $('.jconfirm');
            if (a.eq(a.length - 1)[0] !== this.$el[0])
                return false;

            var key = e.which;
            if (key === 27) {
                /*
                 * if ESC key
                 */
                if (!this.backgroundDismiss) {
                    /*
                     * If background dismiss is false, Glow the modal.
                     */
                    this.$el.find('.jconfirm-bg').click();
                    return false;
                }

                if (this.$cancelButton) {
                    this.$cancelButton.click();
                } else {
                    this.close();
                }
            }
            if (key === 13 || key == 32) {
                /*
                 * if ENTER or SPACE key
                 */
                if (this.$confirmButton) {
                    this.$confirmButton.click();
                } else {

                }
            }
        },
        setDialogCenter: function (animate) {
            var windowHeight = $(window).height();
            var boxHeight = this.$b.outerHeight();
            var topMargin = (windowHeight - boxHeight) / 2;
            var minMargin = 100;
            if (boxHeight > (windowHeight - minMargin)) {
                var style = {
                    'margin-top': minMargin / 2,
                    'margin-bottom': minMargin / 2,
                }
            } else {
                var style = {
                    'margin-top': topMargin,
                }
            }
            if (animate) {
                this.$b.animate(style, {
                    duration: (animate) ? this.animationSpeed : 0,
                    queue: false
                });
            } else {
                this.$b.css(style);
            }
        },
        close: function () {
            var that = this;

            /*
             unbind the window resize & keyup event.
             */
            $(window).unbind('resize.' + this._rand);
            if (this.keyboardEnabled)
                $(window).unbind('keyup.' + this._rand);

            that.$el.find('.jconfirm-bg').removeClass('seen');
            this.$b.addClass(this.animation);

            if(!jconfirm.record.currentlyOpen)
                $('body').removeClass('jconfirm-noscroll');

            setTimeout(function () {
                that.$el.remove();
            }, this.animationSpeed + 30); // wait 30 miliseconds more, ensure everything is done.

            jconfirm.record.closed += 1;
            jconfirm.record.currentlyOpen -= 1;
        },
        open: function () {
            var that = this;
            if (this.isClosed())
                return false;

            that.$el.find('.jconfirm-bg').addClass('seen');

            $('body').addClass('jconfirm-noscroll');
            this.$b.removeClass(this.animations.join(' '));
            /**
             * Blur the focused elements, prevents re-execution with button press.
             */
            $('body :focus').trigger('blur');
            this.$b.find('input[autofocus]:visible:first').focus();
            jconfirm.record.opened += 1;
            jconfirm.record.currentlyOpen += 1;
            return true;
        },
        isClosed: function () {
            return (this.$el.css('display') === '') ? true : false;
        }
    };

    jconfirm.pluginDefaults = {
        template: '

',
        title: 'Hello',
        content: 'Are you sure to continue?',
        contentLoaded: function () {
        },
        icon: '',
        confirmButton: 'Okay',
        cancelButton: 'Cancel',
        confirmButtonClass: 'btn-default',
        cancelButtonClass: 'btn-default',
        theme: 'white',
        animation: 'scale',
        animationSpeed: 400,
        animationBounce: 1.5,
        keyboardEnabled: false,
        container: 'body',
        confirm: function () {
        },
        cancel: function () {
        },
        backgroundDismiss: true,
        autoClose: false,
        closeIcon: null,
        columnClass: 'col-md-6 col-md-offset-3',
    };

    jconfirm.record = {
        opened: 0,
        closed: 0,
        currentlyOpen: 0,
    };
})(jQuery);
<|endoftext|>