<commit_before>const tabListener        = require('binary-style').tabListener;
const Url                = require('./url');
const applyToAllElements = require('./utility').applyToAllElements;

const TabSelector = (() => {
    // obj_tabs will be built in the following format:
    // obj_tabs = { first_tab_group_selector_id: { id_tabs: [ id_of_tab_one, id_of_tab_two ] }
    // we will use id_tabs to handle which tab to show when going to the left or right tab
    const obj_tabs = {};

    const onLoad = () => {
        tabListener();
        applyToAllElements('.tab-selector-wrapper .tm-ul', (tab_selector) => {
            const tab_selector_id = tab_selector.getAttribute('id');
            applyToAllElements('.tm-li', (tab) => {
                if (!/tab-selector/.test(tab.className)) {
                    const tab_id = tab.getAttribute('id');
                    if (!obj_tabs[tab_selector_id]) {
                        obj_tabs[tab_selector_id] = { id_tabs: [] };
                    }
                    obj_tabs[tab_selector_id].id_tabs.push(tab_id);
                }
                tab.addEventListener('click', slideSelectorOnMenuClick);
            }, '', tab_selector);
        });
        // set initial width and margin-left of tab selector
        repositionSelector();
        window.addEventListener('resize', repositionSelector);

        applyToAllElements('.go-left', (element) => {
            element.addEventListener('click', goLeft);
        });
        applyToAllElements('.go-right', (element) => {
            element.addEventListener('click', goRight);
        });
    };

    const repositionSelector = () => {
        const params_hash = Url.paramsHash();
        Object.keys(obj_tabs).forEach((tab_id) => {
            const id_to_show = params_hash[tab_id] || obj_tabs[tab_id].id_tabs[0];
            const el_to_show = document.getElementById(id_to_show);
            const selector   = el_to_show.parentNode.getAttribute('id');
            changeTab({ selector, el_to_show });
        });
    };

    const slideSelectorOnMenuClick = (e) => {
        if (e.target.nodeName !== 'A' || /a-active/.test(e.target.classList)) {
            return;
        }
        const selector = e.target.closest('ul').getAttribute('id');
        slideSelector(selector, e.target);
        updateURL(selector, e.target.parentNode.getAttribute('id'));
    };

    const updateURL = (selector, tab_id) => {
        const params_hash = Url.paramsHash();
        params_hash[selector] = tab_id;
        const updated_url = `${window.location.origin}${window.location.pathname}?${Url.paramsHashToString(params_hash)}`;
        window.history.replaceState({ url: updated_url }, null, updated_url);
    };

    const goLeft = (e) => {
        changeTab({ selector: e.target.getAttribute('data-parent'), direction: 'left' });
    };

    const goRight = (e) => {
        changeTab({ selector: e.target.getAttribute('data-parent'), direction: 'right' });
    };

    const changeTab = (options) => {
        const params_hash     = Url.paramsHash();
        const arr_id_tabs     = obj_tabs[options.selector].id_tabs;
        const id_selected_tab = params_hash[options.selector] || obj_tabs[options.selector].id_tabs[0];
        const current_index   = arr_id_tabs.indexOf(id_selected_tab);
        let index_to_show = current_index;
        if (options.direction) {
            if (options.direction === 'left') {
                index_to_show = current_index > 0 ? current_index - 1 : arr_id_tabs.length - 1;
            } else {
                index_to_show = current_index === arr_id_tabs.length - 1 ? 0 : current_index + 1;
            }
            options.el_to_show = document.getElementById(arr_id_tabs[index_to_show]);
            updateURL(options.selector, arr_id_tabs[index_to_show]);
        }

        if (!options.el_to_show || !options.selector) {
            return;
        }

        selectCircle(options.selector, current_index, index_to_show);
        slideSelector(options.selector, options.el_to_show);
        showContent(options.selector, document.getElementById(id_selected_tab), options.el_to_show);

        if (params_hash.section) {
            setTimeout(() => { $.scrollTo($(`#${params_hash.section}`), 500, { offset: -10 }); }, 500);
        }
    };

    const slideSelector = (selector, el_to_show) => {
        document.getElementById(`${selector}_selector`).setAttribute('style', `width: ${el_to_show.offsetWidth}px; margin-left: ${el_to_show.offsetLeft}px;`);
    };

    const selectCircle = (selector, old_index, index_to_show) => {
        const el_circle = document.getElementById(`${selector}_circles`);
        if (el_circle) {
            const all_circles = el_circle.children;
            all_circles[old_index].classList.remove('selected');
            all_circles[index_to_show].classList.add('selected');
        }
    };

    const showContent = (selector, old_content, new_content) => {
        old_content.classList.remove('active');
        document.getElementById(`${old_content.getAttribute('id')}-content`).classList.add('invisible');
        new_content.classList.add('active');
        document.getElementById(`${new_content.getAttribute('id')}-content`).classList.remove('invisible');
    };

    const onUnload = () => {
        window.removeEventListener('resize', repositionSelector);

        applyToAllElements('.tm-li', (element) => {
            element.removeEventListener('click', slideSelectorOnMenuClick);
        });

        applyToAllElements('.go-left', (element) => {
            element.removeEventListener('click', goLeft);
        });
        applyToAllElements('.go-right', (element) => {
            element.removeEventListener('click', goRight);
        });
    };

    return {
        onLoad,
        onUnload,
    };
})();

module.exports = TabSelector;
<commit_msg>empty obj_tabs on load<commit_after>const tabListener        = require('binary-style').tabListener;
const Url                = require('./url');
const applyToAllElements = require('./utility').applyToAllElements;

const TabSelector = (() => {
    // obj_tabs will be built in the following format:
    // obj_tabs = { first_tab_group_selector_id: { id_tabs: [ id_of_tab_one, id_of_tab_two ] }
    // we will use id_tabs to handle which tab to show when going to the left or right tab
    let obj_tabs = {};

    const onLoad = () => {
        tabListener();
        obj_tabs = {};
        applyToAllElements('.tab-selector-wrapper .tm-ul', (tab_selector) => {
            const tab_selector_id = tab_selector.getAttribute('id');
            applyToAllElements('.tm-li', (tab) => {
                if (!/tab-selector/.test(tab.className)) {
                    const tab_id = tab.getAttribute('id');
                    if (!obj_tabs[tab_selector_id]) {
                        obj_tabs[tab_selector_id] = { id_tabs: [] };
                    }
                    obj_tabs[tab_selector_id].id_tabs.push(tab_id);
                }
                tab.addEventListener('click', slideSelectorOnMenuClick);
            }, '', tab_selector);
        });
        // set initial width and margin-left of tab selector
        repositionSelector();
        window.addEventListener('resize', repositionSelector);

        applyToAllElements('.go-left', (element) => {
            element.addEventListener('click', goLeft);
        });
        applyToAllElements('.go-right', (element) => {
            element.addEventListener('click', goRight);
        });
    };

    const repositionSelector = () => {
        const params_hash = Url.paramsHash();
        Object.keys(obj_tabs).forEach((tab_id) => {
            const id_to_show = params_hash[tab_id] || obj_tabs[tab_id].id_tabs[0];
            const el_to_show = document.getElementById(id_to_show);
            const selector   = el_to_show.parentNode.getAttribute('id');
            changeTab({ selector, el_to_show });
        });
    };

    const slideSelectorOnMenuClick = (e) => {
        if (e.target.nodeName !== 'A' || /a-active/.test(e.target.classList)) {
            return;
        }
        const selector = e.target.closest('ul').getAttribute('id');
        slideSelector(selector, e.target);
        updateURL(selector, e.target.parentNode.getAttribute('id'));
    };

    const updateURL = (selector, tab_id) => {
        const params_hash = Url.paramsHash();
        params_hash[selector] = tab_id;
        const updated_url = `${window.location.origin}${window.location.pathname}?${Url.paramsHashToString(params_hash)}`;
        window.history.replaceState({ url: updated_url }, null, updated_url);
    };

    const goLeft = (e) => {
        changeTab({ selector: e.target.getAttribute('data-parent'), direction: 'left' });
    };

    const goRight = (e) => {
        changeTab({ selector: e.target.getAttribute('data-parent'), direction: 'right' });
    };

    const changeTab = (options) => {
        const params_hash     = Url.paramsHash();
        const arr_id_tabs     = obj_tabs[options.selector].id_tabs;
        const id_selected_tab = params_hash[options.selector] || obj_tabs[options.selector].id_tabs[0];
        const current_index   = arr_id_tabs.indexOf(id_selected_tab);
        let index_to_show = current_index;
        if (options.direction) {
            if (options.direction === 'left') {
                index_to_show = current_index > 0 ? current_index - 1 : arr_id_tabs.length - 1;
            } else {
                index_to_show = current_index === arr_id_tabs.length - 1 ? 0 : current_index + 1;
            }
            options.el_to_show = document.getElementById(arr_id_tabs[index_to_show]);
            updateURL(options.selector, arr_id_tabs[index_to_show]);
        }

        if (!options.el_to_show || !options.selector) {
            return;
        }

        selectCircle(options.selector, current_index, index_to_show);
        slideSelector(options.selector, options.el_to_show);
        showContent(options.selector, document.getElementById(id_selected_tab), options.el_to_show);

        if (params_hash.section) {
            setTimeout(() => { $.scrollTo($(`#${params_hash.section}`), 500, { offset: -10 }); }, 500);
        }
    };

    const slideSelector = (selector, el_to_show) => {
        document.getElementById(`${selector}_selector`).setAttribute('style', `width: ${el_to_show.offsetWidth}px; margin-left: ${el_to_show.offsetLeft}px;`);
    };

    const selectCircle = (selector, old_index, index_to_show) => {
        const el_circle = document.getElementById(`${selector}_circles`);
        if (el_circle) {
            const all_circles = el_circle.children;
            all_circles[old_index].classList.remove('selected');
            all_circles[index_to_show].classList.add('selected');
        }
    };

    const showContent = (selector, old_content, new_content) => {
        old_content.classList.remove('active');
        document.getElementById(`${old_content.getAttribute('id')}-content`).classList.add('invisible');
        new_content.classList.add('active');
        document.getElementById(`${new_content.getAttribute('id')}-content`).classList.remove('invisible');
    };

    const onUnload = () => {
        window.removeEventListener('resize', repositionSelector);

        applyToAllElements('.tm-li', (element) => {
            element.removeEventListener('click', slideSelectorOnMenuClick);
        });

        applyToAllElements('.go-left', (element) => {
            element.removeEventListener('click', goLeft);
        });
        applyToAllElements('.go-right', (element) => {
            element.removeEventListener('click', goRight);
        });
    };

    return {
        onLoad,
        onUnload,
    };
})();

module.exports = TabSelector;
<|endoftext|>


<commit_before>'use strict';

import BaseStore from 'fluxible/addons/BaseStore';
import getReleases from '../services/getReleases';

class ReleasesStore extends BaseStore {
    constructor (dispatcher) {
        super(dispatcher);
        this.releases;
    }

    handleGetReleases (payload) {
        this.releases = getReleases.releases();
        this.emitChange();
    }

    getState () {
        return {
            releases: this.releases
        };
    }

    dehydrate () {
        return this.getState();
    }

    rehydrate (state) {
        this.releases = getReleases.releases();
    }

}

ReleasesStore.storeName = 'ReleasesStore';
ReleasesStore.handlers = {
    'GET_RELEASES': 'handleGetReleases'
};

export default ReleasesStore;
<commit_msg>update `ReleasesStore` to use `api` service<commit_after>'use strict';

import BaseStore from 'fluxible/addons/BaseStore';
import api from '../services/api';

class ReleasesStore extends BaseStore {
    constructor (dispatcher) {
        super(dispatcher);
        this.releases = undefined;
    }

    handleGetReleases (payload) {
        this.requestReleases();
    }

    requestReleases () {
        this.releases = api(this, 'http://api.archaichorizon.com/releases.json');
    }

    setState (releases) {
        this.releases = releases;
        this.emitChange();
    }

    getState () {
        return this.releases;
    }

    dehydrate () {
        return this.getState();
    }

    rehydrate (state) {
        this.releases = getReleases.releases();
    }

}

ReleasesStore.storeName = 'ReleasesStore';
ReleasesStore.handlers = {
    'GET_RELEASES': 'handleGetReleases'
};

export default ReleasesStore;
<|endoftext|>


<commit_before>var POWERUP_ICONS = [
  [
    ['moveTo',  8, -6],
    ['lineTo', 12, -2],
    ['lineTo',  8,  6],
    ['lineTo',  4, -2],
    ['lineTo',  8, -6],

    ['moveTo',  -8, -6],
    ['lineTo',  -4, -2],
    ['lineTo',  -8,  6],
    ['lineTo', -12, -2],
    ['lineTo',  -8, -6],
  ],
  [
    ['moveTo', 0, -PLAYER_RADIUS],
    ['lineTo', (PLAYER_RADIUS / 2) * Math.cos(Math.PI / -4), (PLAYER_RADIUS / 2) * Math.sin(Math.PI / -4)],
    ['lineTo', PLAYER_RADIUS, 0],
    ['lineTo', (PLAYER_RADIUS * 1.25) * Math.cos(Math.PI * 1 / 4), (PLAYER_RADIUS * 1.25) * Math.sin(Math.PI * 1 / 4)],
    ['lineTo', 0, PLAYER_RADIUS / 2],
    ['lineTo', (PLAYER_RADIUS * 1.25) * Math.cos(Math.PI * 3 / 4), (PLAYER_RADIUS * 1.25) * Math.sin(Math.PI * 3 / 4)],
    ['lineTo', -PLAYER_RADIUS, 0],
    ['lineTo', (PLAYER_RADIUS / 2) * Math.cos(Math.PI * 3 / -4), (PLAYER_RADIUS / 2) * Math.sin(Math.PI * 3 / -4)],
  ],
  [
    ['moveTo',  0, -8],
    ['lineTo',  3,  3],
    ['lineTo',  3,  4],
    ['lineTo',  5,  4],
    ['lineTo',  5,  10],
    ['lineTo',  4,  10],
    ['lineTo',  1,  7],
    ['lineTo', -1,  7],
    ['lineTo', -4,  10],
    ['lineTo', -5,  10],
    ['lineTo', -5,  4],
    ['lineTo', -3,  4],
    ['lineTo', -3,  3],
  ],
  [
    ['moveTo', -12,  12],
    ['lineTo', -12,   6],
    ['lineTo',   6, -12],
    ['lineTo',  12,  -6],
    ['lineTo', -6,   12]
  ],
  [
    ['moveTo', 4, 0],
    ['arc', 0, 0, 4, 0, 2 * Math.PI],

    ['moveTo', 9, 0],
    ['arc', 0, 0, 9, 0, 2 * Math.PI],

    ['moveTo', 14, 0],
    ['arc', 0, 0, 14, 0, 2 * Math.PI]
  ],
  [
    ['moveTo', 16 * Math.cos(3 * Math.PI / 4), 16 * Math.sin(3 * Math.PI / 4)],
    ['lineTo',  5 * Math.cos(5 * Math.PI / 4),  5 * Math.sin(5 * Math.PI / 4)],
    ['lineTo',  2 * Math.cos(5 * Math.PI / 4),  2 * Math.sin(5 * Math.PI / 4)],
    ['lineTo',  2 * Math.cos(5 * Math.PI / 4),  2 * Math.sin(5 * Math.PI / 4)],
    ['lineTo', 16 * Math.cos(7 * Math.PI / 4), 16 * Math.sin(7 * Math.PI / 4)],
    ['lineTo',  5 * Math.cos(Math.PI / 4),  5 * Math.sin(Math.PI / 4)],
    ['lineTo',  2 * Math.cos(Math.PI / 4),  2 * Math.sin(Math.PI / 4)]
  ]
];
var POWERUP_COLORS = [
  ['#7788CC', '#FFFF88',   '#88FF88', 2],
  ['#77AA44', '#44998B',   '#FFDD77', 3],
  ['#CC5555', '#CCCCCC',   '#555555', 2],
  ['#552277', '#FFFFFF',   '#EEAA22', 2],
  ['#CC5511', '#11AA1155', '#22EE22', 1],
  ['#EEEE22', '#3377DD',   '#77AAFF', 1]
];

var Powerup = function(x, y, type) {
  this.x = x;
  this.y = y;
  this.type = type;
  this.size = POWERUP_SIZE;
  this.frame = 0;

  return this;
}
Powerup.lifetime = 18;
Powerup.types = ['bolt', 'clone', 'speed', 'battery'];
Powerup.outerPath = [
  ['moveTo', -POWERUP_SIZE, 6 - POWERUP_SIZE],
  ['lineTo', 6 - POWERUP_SIZE, -POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 6, -POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE, 6 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE, POWERUP_SIZE - 6],
  ['lineTo', POWERUP_SIZE - 6, POWERUP_SIZE],
  ['lineTo', 6 - POWERUP_SIZE, POWERUP_SIZE],
  ['lineTo', -POWERUP_SIZE, POWERUP_SIZE - 6],
];
Powerup.innerPath = [
  ['moveTo', 5 - POWERUP_SIZE, 8 - POWERUP_SIZE],
  ['lineTo', 8 - POWERUP_SIZE, 5 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 8, 5 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 5, 8 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 5, POWERUP_SIZE - 8],
  ['lineTo', POWERUP_SIZE - 8, POWERUP_SIZE - 5],
  ['lineTo', 8 - POWERUP_SIZE, POWERUP_SIZE - 5],
  ['lineTo', 5 - POWERUP_SIZE, POWERUP_SIZE - 8],
];
Powerup.prototype.collide = function() {
  return this.frame == 0 && gPlayer.x >= this.x - POWERUP_SIZE && gPlayer.y >= this.y - POWERUP_SIZE && gPlayer.x <= this.x + POWERUP_SIZE && gPlayer.y <= this.y + POWERUP_SIZE;
}
Powerup.prototype.getType = function() {
  return Powerup.types[this.type];
}
Powerup.prototype.render = function() {
  var strokeStyle, lineWidth, fillStyle;
  var opacity = this.frame > 0 && this.frame < Powerup.lifetime ? decToHex(255 * ((Powerup.lifetime - this.frame) / Powerup.lifetime)) : 'FF';

  renderPath(Powerup.outerPath, true, this.x - gCamera.x, this.y - gCamera.y);
  fill(POWERUP_COLORS[this.type][0] + opacity);
  stroke('#222222' + opacity, 2);
  renderPath(Powerup.innerPath, true, this.x - gCamera.x, this.y - gCamera.y);
  stroke();

  if (this.frame === 0) {
    renderPath(POWERUP_ICONS[this.type], true, this.x - gCamera.x, this.y - gCamera.y);
    fill(POWERUP_COLORS[this.type][1]);
    stroke(POWERUP_COLORS[this.type][2], POWERUP_COLORS[this.type][3]);
  }
  else this.frame++;
};
<commit_msg>Add new powerup types to Powerup.types<commit_after>var POWERUP_ICONS = [
  [
    ['moveTo',  8, -6],
    ['lineTo', 12, -2],
    ['lineTo',  8,  6],
    ['lineTo',  4, -2],
    ['lineTo',  8, -6],

    ['moveTo',  -8, -6],
    ['lineTo',  -4, -2],
    ['lineTo',  -8,  6],
    ['lineTo', -12, -2],
    ['lineTo',  -8, -6],
  ],
  [
    ['moveTo', 0, -PLAYER_RADIUS],
    ['lineTo', (PLAYER_RADIUS / 2) * Math.cos(Math.PI / -4), (PLAYER_RADIUS / 2) * Math.sin(Math.PI / -4)],
    ['lineTo', PLAYER_RADIUS, 0],
    ['lineTo', (PLAYER_RADIUS * 1.25) * Math.cos(Math.PI * 1 / 4), (PLAYER_RADIUS * 1.25) * Math.sin(Math.PI * 1 / 4)],
    ['lineTo', 0, PLAYER_RADIUS / 2],
    ['lineTo', (PLAYER_RADIUS * 1.25) * Math.cos(Math.PI * 3 / 4), (PLAYER_RADIUS * 1.25) * Math.sin(Math.PI * 3 / 4)],
    ['lineTo', -PLAYER_RADIUS, 0],
    ['lineTo', (PLAYER_RADIUS / 2) * Math.cos(Math.PI * 3 / -4), (PLAYER_RADIUS / 2) * Math.sin(Math.PI * 3 / -4)],
  ],
  [
    ['moveTo',  0, -8],
    ['lineTo',  3,  3],
    ['lineTo',  3,  4],
    ['lineTo',  5,  4],
    ['lineTo',  5,  10],
    ['lineTo',  4,  10],
    ['lineTo',  1,  7],
    ['lineTo', -1,  7],
    ['lineTo', -4,  10],
    ['lineTo', -5,  10],
    ['lineTo', -5,  4],
    ['lineTo', -3,  4],
    ['lineTo', -3,  3],
  ],
  [
    ['moveTo', -12,  12],
    ['lineTo', -12,   6],
    ['lineTo',   6, -12],
    ['lineTo',  12,  -6],
    ['lineTo', -6,   12]
  ],
  [
    ['moveTo', 4, 0],
    ['arc', 0, 0, 4, 0, 2 * Math.PI],

    ['moveTo', 9, 0],
    ['arc', 0, 0, 9, 0, 2 * Math.PI],

    ['moveTo', 14, 0],
    ['arc', 0, 0, 14, 0, 2 * Math.PI]
  ],
  [
    ['moveTo', 16 * Math.cos(3 * Math.PI / 4), 16 * Math.sin(3 * Math.PI / 4)],
    ['lineTo',  5 * Math.cos(5 * Math.PI / 4),  5 * Math.sin(5 * Math.PI / 4)],
    ['lineTo',  2 * Math.cos(5 * Math.PI / 4),  2 * Math.sin(5 * Math.PI / 4)],
    ['lineTo',  2 * Math.cos(5 * Math.PI / 4),  2 * Math.sin(5 * Math.PI / 4)],
    ['lineTo', 16 * Math.cos(7 * Math.PI / 4), 16 * Math.sin(7 * Math.PI / 4)],
    ['lineTo',  5 * Math.cos(Math.PI / 4),  5 * Math.sin(Math.PI / 4)],
    ['lineTo',  2 * Math.cos(Math.PI / 4),  2 * Math.sin(Math.PI / 4)]
  ]
];
var POWERUP_COLORS = [
  ['#7788CC', '#FFFF88',   '#88FF88', 2],
  ['#77AA44', '#44998B',   '#FFDD77', 3],
  ['#CC5555', '#CCCCCC',   '#555555', 2],
  ['#552277', '#FFFFFF',   '#EEAA22', 2],
  ['#CC5511', '#11AA1155', '#22EE22', 1],
  ['#EEEE22', '#3377DD',   '#77AAFF', 1]
];

var Powerup = function(x, y, type) {
  this.x = x;
  this.y = y;
  this.type = type;
  this.size = POWERUP_SIZE;
  this.frame = 0;

  return this;
}
Powerup.lifetime = 18;
Powerup.types = ['bolt', 'clone', 'missile', 'beam', 'shield', 'armor'];
Powerup.outerPath = [
  ['moveTo', -POWERUP_SIZE, 6 - POWERUP_SIZE],
  ['lineTo', 6 - POWERUP_SIZE, -POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 6, -POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE, 6 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE, POWERUP_SIZE - 6],
  ['lineTo', POWERUP_SIZE - 6, POWERUP_SIZE],
  ['lineTo', 6 - POWERUP_SIZE, POWERUP_SIZE],
  ['lineTo', -POWERUP_SIZE, POWERUP_SIZE - 6],
];
Powerup.innerPath = [
  ['moveTo', 5 - POWERUP_SIZE, 8 - POWERUP_SIZE],
  ['lineTo', 8 - POWERUP_SIZE, 5 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 8, 5 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 5, 8 - POWERUP_SIZE],
  ['lineTo', POWERUP_SIZE - 5, POWERUP_SIZE - 8],
  ['lineTo', POWERUP_SIZE - 8, POWERUP_SIZE - 5],
  ['lineTo', 8 - POWERUP_SIZE, POWERUP_SIZE - 5],
  ['lineTo', 5 - POWERUP_SIZE, POWERUP_SIZE - 8],
];
Powerup.prototype.collide = function() {
  return this.frame == 0 && gPlayer.x >= this.x - POWERUP_SIZE && gPlayer.y >= this.y - POWERUP_SIZE && gPlayer.x <= this.x + POWERUP_SIZE && gPlayer.y <= this.y + POWERUP_SIZE;
}
Powerup.prototype.getType = function() {
  return Powerup.types[this.type];
}
Powerup.prototype.render = function() {
  var strokeStyle, lineWidth, fillStyle;
  var opacity = this.frame > 0 && this.frame < Powerup.lifetime ? decToHex(255 * ((Powerup.lifetime - this.frame) / Powerup.lifetime)) : 'FF';

  renderPath(Powerup.outerPath, true, this.x - gCamera.x, this.y - gCamera.y);
  fill(POWERUP_COLORS[this.type][0] + opacity);
  stroke('#222222' + opacity, 2);
  renderPath(Powerup.innerPath, true, this.x - gCamera.x, this.y - gCamera.y);
  stroke();

  if (this.frame === 0) {
    renderPath(POWERUP_ICONS[this.type], true, this.x - gCamera.x, this.y - gCamera.y);
    fill(POWERUP_COLORS[this.type][1]);
    stroke(POWERUP_COLORS[this.type][2], POWERUP_COLORS[this.type][3]);
  }
  else this.frame++;
};
<|endoftext|>


<commit_before>import React from 'react';
import Column from '../ui/components/column';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../../initial_state';
import { fetchFollowRequests } from '../../actions/accounts';
import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom';
import { fetchTrends } from '../../actions/trends';
import Hashtag from '../../components/hashtag';
import NavigationBar from '../compose/components/navigation_bar';

const messages = defineMessages({
  home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
  notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
  settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
  area_timeline: { id: 'navigation_bar.area_timeline', defaultMessage: 'Area timeline' },
  direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
  refresh_trends: { id: 'trends.refresh', defaultMessage: 'Refresh' },
  discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
  personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
  security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
});

const mapStateToProps = state => ({
  myAccount: state.getIn(['accounts', me]),
  unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
  trends: state.get('trends'),
});

const mapDispatchToProps = dispatch => ({
  fetchFollowRequests: () => dispatch(fetchFollowRequests()),
  fetchTrends: () => dispatch(fetchTrends()),
});

const badgeDisplay = (number, limit) => {
  if (number === 0) {
    return undefined;
  } else if (limit && number >= limit) {
    return `${limit}+`;
  } else {
    return number;
  }
};

@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class GettingStarted extends ImmutablePureComponent {

  static propTypes = {
    intl: PropTypes.object.isRequired,
    myAccount: ImmutablePropTypes.map.isRequired,
    columns: ImmutablePropTypes.list,
    multiColumn: PropTypes.bool,
    fetchFollowRequests: PropTypes.func.isRequired,
    unreadFollowRequests: PropTypes.number,
    unreadNotifications: PropTypes.number,
    trends: ImmutablePropTypes.list,
  };

  componentDidMount () {
    const { myAccount, fetchFollowRequests } = this.props;

    if (myAccount.get('locked')) {
      fetchFollowRequests();
    }

    setTimeout(() => this.props.fetchTrends(), 5000);
  }

  render () {
    const { intl, myAccount, multiColumn, unreadFollowRequests, trends } = this.props;

    const navItems = [];

    if (multiColumn) {
      navItems.push(
        <ColumnSubheading key='1' text={intl.formatMessage(messages.discover)} />,
        <ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
        <ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
        <ColumnSubheading key='8' text={intl.formatMessage(messages.personal)} />
      );
    }

      }

      if (!columns.find(item => item.get('id') === 'AREA')) {
        navItems.push(<ColumnLink key='20' icon='map-marker' text={intl.formatMessage(messages.area_timeline)} to='/timelines/area' />);
    navItems.push(
      <ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
      <ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
      <ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
    );

    if (myAccount.get('locked')) {
      navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
    }

    if (!multiColumn) {
      navItems.push(
        <ColumnSubheading key='9' text={intl.formatMessage(messages.settings_subheading)} />,
        <ColumnLink key='6' icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
        <ColumnLink key='6' icon='lock' text={intl.formatMessage(messages.security)} href='/auth/edit' />
      );
    }

    return (
      <Column>
        {multiColumn &&

            <button>

              <FormattedMessage id='getting_started.heading' defaultMessage='Getting started' />
            </button>

}

          {!multiColumn && <NavigationBar account={myAccount} />}
          {navItems}

        {multiColumn && trends &&

              <button>

                <FormattedMessage id='trends.header' defaultMessage='Trending now' />
              </button>

                <button className='column-header__button' title={intl.formatMessage(messages.refresh_trends)} aria-label={intl.formatMessage(messages.refresh_trends)}></button>

{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}

}

        {!multiColumn &&
}

            {multiColumn &&
<Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> ·
}

<FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /> ·

<FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /> ·

<FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /> ·

<FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' />

            <FormattedMessage
              id='getting_started.open_source_notice'
              defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
              values={{ github: tootsuite/mastodon }}
            />

      </Column>
    );
  }

}
<commit_msg>マージ修正<commit_after>import React from 'react';
import Column from '../ui/components/column';
import ColumnLink from '../ui/components/column_link';
import ColumnSubheading from '../ui/components/column_subheading';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { me } from '../../initial_state';
import { fetchFollowRequests } from '../../actions/accounts';
import { List as ImmutableList } from 'immutable';
import { Link } from 'react-router-dom';
import { fetchTrends } from '../../actions/trends';
import Hashtag from '../../components/hashtag';
import NavigationBar from '../compose/components/navigation_bar';

const messages = defineMessages({
  home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
  notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
  public_timeline: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
  settings_subheading: { id: 'column_subheading.settings', defaultMessage: 'Settings' },
  community_timeline: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
  area_timeline: { id: 'navigation_bar.area_timeline', defaultMessage: 'Area timeline' },
  direct: { id: 'navigation_bar.direct', defaultMessage: 'Direct messages' },
  preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
  follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
  favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
  blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
  domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
  mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
  pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
  lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
  refresh_trends: { id: 'trends.refresh', defaultMessage: 'Refresh' },
  discover: { id: 'navigation_bar.discover', defaultMessage: 'Discover' },
  personal: { id: 'navigation_bar.personal', defaultMessage: 'Personal' },
  security: { id: 'navigation_bar.security', defaultMessage: 'Security' },
});

const mapStateToProps = state => ({
  myAccount: state.getIn(['accounts', me]),
  unreadFollowRequests: state.getIn(['user_lists', 'follow_requests', 'items'], ImmutableList()).size,
  trends: state.get('trends'),
});

const mapDispatchToProps = dispatch => ({
  fetchFollowRequests: () => dispatch(fetchFollowRequests()),
  fetchTrends: () => dispatch(fetchTrends()),
});

const badgeDisplay = (number, limit) => {
  if (number === 0) {
    return undefined;
  } else if (limit && number >= limit) {
    return `${limit}+`;
  } else {
    return number;
  }
};

@connect(mapStateToProps, mapDispatchToProps)
@injectIntl
export default class GettingStarted extends ImmutablePureComponent {

  static propTypes = {
    intl: PropTypes.object.isRequired,
    myAccount: ImmutablePropTypes.map.isRequired,
    columns: ImmutablePropTypes.list,
    multiColumn: PropTypes.bool,
    fetchFollowRequests: PropTypes.func.isRequired,
    unreadFollowRequests: PropTypes.number,
    unreadNotifications: PropTypes.number,
    trends: ImmutablePropTypes.list,
  };

  componentDidMount () {
    const { myAccount, fetchFollowRequests } = this.props;

    if (myAccount.get('locked')) {
      fetchFollowRequests();
    }

    setTimeout(() => this.props.fetchTrends(), 5000);
  }

  render () {
    const { intl, myAccount, multiColumn, unreadFollowRequests, trends } = this.props;

    const navItems = [];

    if (multiColumn) {
      navItems.push(
        <ColumnSubheading key='1' text={intl.formatMessage(messages.discover)} />,
        <ColumnLink key='2' icon='users' text={intl.formatMessage(messages.community_timeline)} to='/timelines/public/local' />,
        <ColumnLink key='3' icon='globe' text={intl.formatMessage(messages.public_timeline)} to='/timelines/public' />,
        <ColumnLink key='20' icon='map-marker' text={intl.formatMessage(messages.area_timeline)} to='/timelines/area' />,
        <ColumnSubheading key='8' text={intl.formatMessage(messages.personal)} />
      );
    }

    navItems.push(
      <ColumnLink key='4' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
      <ColumnLink key='5' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
      <ColumnLink key='6' icon='bars' text={intl.formatMessage(messages.lists)} to='/lists' />
    );

    if (myAccount.get('locked')) {
      navItems.push(<ColumnLink key='7' icon='users' text={intl.formatMessage(messages.follow_requests)} badge={badgeDisplay(unreadFollowRequests, 40)} to='/follow_requests' />);
    }

    if (!multiColumn) {
      navItems.push(
        <ColumnSubheading key='9' text={intl.formatMessage(messages.settings_subheading)} />,
        <ColumnLink key='6' icon='gears' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' />,
        <ColumnLink key='6' icon='lock' text={intl.formatMessage(messages.security)} href='/auth/edit' />
      );
    }

    return (
      <Column>
        {multiColumn &&

            <button>

              <FormattedMessage id='getting_started.heading' defaultMessage='Getting started' />
            </button>

}

          {!multiColumn && <NavigationBar account={myAccount} />}
          {navItems}

        {multiColumn && trends &&

              <button>

                <FormattedMessage id='trends.header' defaultMessage='Trending now' />
              </button>

                <button className='column-header__button' title={intl.formatMessage(messages.refresh_trends)} aria-label={intl.formatMessage(messages.refresh_trends)}></button>

{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}

}

        {!multiColumn &&
}

            {multiColumn &&
<Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> ·
}

<FormattedMessage id='navigation_bar.info' defaultMessage='About this instance' /> ·

<FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /> ·

<FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /> ·

<FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' />

            <FormattedMessage
              id='getting_started.open_source_notice'
              defaultMessage='Mastodon is open source software. You can contribute or report issues on GitHub at {github}.'
              values={{ github: tootsuite/mastodon }}
            />

      </Column>
    );
  }

}
<|endoftext|>


<commit_before>/*
Copyright (C) 2016 andrea rota <a@xelera.eu>

This file is part of Pattrn - http://pattrn.co/.

Pattrn is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Pattrn is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Pattrn.  If not, see <http://www.gnu.org/licenses/>.
*/

import { bisect_features } from './utils/bisect.js';
import { is_defined } from'./utils/is_defined.js';

/**
 * Translate GeoJSON source data to the legacy format that
 * Pattrn v1 expects.
 * Only FeatureCollections comprising Points are supported at the moment
 * @param Object data The GeoJSON feature collection
 * @param Object The dataset structured as Pattrn v1 expects
 */
export function geojson_to_pattrn_legacy_data_structure(data, dataset_metadata, config, settings) {
  var features = [];
  var variables = dataset_metadata.variables;

  if(! is_defined(data) || ! is_defined(data.features) || ! Array.isArray(data.features)) {
    throw 'No GeoJSON feature defined';
  }

  features = data.features
    /**
     * Extract key variables from source GeoJSON data,
     * mapping them to a flat structure as per Pattrn v1
     * source data structure.
     * @x-technical-debt: validate first; reject items without valid lat/lon,
     * date_time, and make sure we never try to access undefined values
     */
    .map(function(value, index, array) {
      var data = {
        event_ID: index,
        location_name: is_defined(value.properties.pattrn_location_name) ? value.properties.pattrn_location_name : null,
        latitude: value.geometry.coordinates[1],
        longitude: value.geometry.coordinates[0],
        geo_accuracy: null,
        date_time: is_defined(value.properties.pattrn_date_time) ? value.properties.pattrn_date_time : null,
        event_summary: is_defined(value.properties.pattrn_event_summary) ? value.properties.pattrn_event_summary : null,
        source_name: null
      };
      var defined_variables = Object.keys(data).length;

      /**
       * Pad variables with dummy empty ones (null is not ok because
       * of lack of error handling before use of variables in legacy
       * code) up to the first integer variable in Pattrn v1 data layout.
       */
      for(var i = 0; i < 8 - defined_variables; i++) {
        data['dummy_base_' + i] = '';
      }

      /**
       * TECHNICAL_DEBT: limit this to 5 until
       * we break from the v1 data layout legacy.
       * Add int variables, if defined
       */
      if(is_defined(variables.integer)) {
        variables.integer.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      /**
       * Pad variables with dummy empty ones (null is not ok because
       * of lack of error handling before use of variables in legacy
       * code) up to the first tag variable in Pattrn v1 data layout.
       */
      defined_variables = Object.keys(data).length;
      for(var i = 0; i < 13 - defined_variables; i++) {
        data['dummy_int_' + i] = '';
      }

      /**
       * TECHNICAL_DEBT: do this only if applicable
       * add source_data_set as first tag variable
       */
      data['source_data_set'] = is_defined(value.properties.pattrn_data_set) ? value.properties.pattrn_data_set : settings.single_data_set;

      /**
       * TECHNICAL_DEBT: limit this to 4 (5 minus the previous one) until
       * we break from the v1 data layout legacy.
       * Add other tag variables, if defined
       */
      if(is_defined(variables.tag)) {
        variables.tag.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      /**
       * Pad variables with dummy empty ones (null is not ok because
       * of lack of error handling before use of variables in legacy
       * code) up to the first bool variable in Pattrn v1 data layout.
       */
      defined_variables = Object.keys(data).length;
      for(var i = 0; i < 18 - defined_variables; i++) {
        data['dummy_tag_' + i] = '';
      }

      /**
       * TECHNICAL_DEBT: limit this to 4 (5 minus the previous one) until
       * we break from the v1 data layout legacy.
       * Add other tag variables, if defined
       */
      if(is_defined(variables.boolean)) {
        variables.boolean.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      /**
       * and finally pad until we have 28 variables as in a complete
       * Pattrn v1 dataset
       */
      defined_variables = Object.keys(data).length;
      for(var i = 0; i < 29 - defined_variables; i++) {
        data['dummy_extra_' + i] = '';
      }

      /**
       * Now add any tree variables - no need to fit them into legacy dataset
       * structure as variables of this type are not supported by legacy Pattrn
       * in any case.
       */
      if(is_defined(variables.tree)) {
        variables.tree.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      data['pattrn_data_set'] = value.properties.pattrn_data_set;
      data['source_variables'] = value.properties;

      return data;
    })
    /**
     * Filter out observations that don't include all of the
     * variables needed (date_time, latitude, longitude).
     */
    .filter(function(value, index, array) {
      return is_defined(value.date_time) && is_defined(value.latitude) && is_defined(value.longitude);
    });

  features = bisect_features(features, config);

  return features;
}
<commit_msg>set pattrn_data_set correctly<commit_after>/*
Copyright (C) 2016 andrea rota <a@xelera.eu>

This file is part of Pattrn - http://pattrn.co/.

Pattrn is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

Pattrn is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with Pattrn.  If not, see <http://www.gnu.org/licenses/>.
*/

import { bisect_features } from './utils/bisect.js';
import { is_defined } from'./utils/is_defined.js';

/**
 * Translate GeoJSON source data to the legacy format that
 * Pattrn v1 expects.
 * Only FeatureCollections comprising Points are supported at the moment
 * @param Object data The GeoJSON feature collection
 * @param Object The dataset structured as Pattrn v1 expects
 */
export function geojson_to_pattrn_legacy_data_structure(data, dataset_metadata, config, settings) {
  var features = [];
  var variables = dataset_metadata.variables;

  if(! is_defined(data) || ! is_defined(data.features) || ! Array.isArray(data.features)) {
    throw 'No GeoJSON feature defined';
  }

  features = data.features
    /**
     * Extract key variables from source GeoJSON data,
     * mapping them to a flat structure as per Pattrn v1
     * source data structure.
     * @x-technical-debt: validate first; reject items without valid lat/lon,
     * date_time, and make sure we never try to access undefined values
     */
    .map(function(value, index, array) {
      var data = {
        event_ID: index,
        location_name: is_defined(value.properties.pattrn_location_name) ? value.properties.pattrn_location_name : null,
        latitude: value.geometry.coordinates[1],
        longitude: value.geometry.coordinates[0],
        geo_accuracy: null,
        date_time: is_defined(value.properties.pattrn_date_time) ? value.properties.pattrn_date_time : null,
        event_summary: is_defined(value.properties.pattrn_event_summary) ? value.properties.pattrn_event_summary : null,
        source_name: null
      };
      var defined_variables = Object.keys(data).length;

      /**
       * Pad variables with dummy empty ones (null is not ok because
       * of lack of error handling before use of variables in legacy
       * code) up to the first integer variable in Pattrn v1 data layout.
       */
      for(var i = 0; i < 8 - defined_variables; i++) {
        data['dummy_base_' + i] = '';
      }

      /**
       * TECHNICAL_DEBT: limit this to 5 until
       * we break from the v1 data layout legacy.
       * Add int variables, if defined
       */
      if(is_defined(variables.integer)) {
        variables.integer.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      /**
       * Pad variables with dummy empty ones (null is not ok because
       * of lack of error handling before use of variables in legacy
       * code) up to the first tag variable in Pattrn v1 data layout.
       */
      defined_variables = Object.keys(data).length;
      for(var i = 0; i < 13 - defined_variables; i++) {
        data['dummy_int_' + i] = '';
      }

      /**
       * TECHNICAL_DEBT: do this only if applicable
       * add source_data_set as first tag variable
       */
      data['source_data_set'] = is_defined(value.properties.pattrn_data_set) ? value.properties.pattrn_data_set : 'single_data_set'; // default_settings.single_data_set.id

      /**
       * TECHNICAL_DEBT: limit this to 4 (5 minus the previous one) until
       * we break from the v1 data layout legacy.
       * Add other tag variables, if defined
       */
      if(is_defined(variables.tag)) {
        variables.tag.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      /**
       * Pad variables with dummy empty ones (null is not ok because
       * of lack of error handling before use of variables in legacy
       * code) up to the first bool variable in Pattrn v1 data layout.
       */
      defined_variables = Object.keys(data).length;
      for(var i = 0; i < 18 - defined_variables; i++) {
        data['dummy_tag_' + i] = '';
      }

      /**
       * TECHNICAL_DEBT: limit this to 4 (5 minus the previous one) until
       * we break from the v1 data layout legacy.
       * Add other tag variables, if defined
       */
      if(is_defined(variables.boolean)) {
        variables.boolean.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      /**
       * and finally pad until we have 28 variables as in a complete
       * Pattrn v1 dataset
       */
      defined_variables = Object.keys(data).length;
      for(var i = 0; i < 29 - defined_variables; i++) {
        data['dummy_extra_' + i] = '';
      }

      /**
       * Now add any tree variables - no need to fit them into legacy dataset
       * structure as variables of this type are not supported by legacy Pattrn
       * in any case.
       */
      if(is_defined(variables.tree)) {
        variables.tree.forEach(function(v, i, a) {
          if(is_defined(v.id)) {
            data[v.id] = value.properties[v.id];
          }
        });
      }

      data['pattrn_data_set'] = data.source_data_set;
      data['source_variables'] = value.properties;

      return data;
    })
    /**
     * Filter out observations that don't include all of the
     * variables needed (date_time, latitude, longitude).
     */
    .filter(function(value, index, array) {
      return is_defined(value.date_time) && is_defined(value.latitude) && is_defined(value.longitude);
    });

  features = bisect_features(features, config);

  return features;
}
<|endoftext|>


<commit_before>import React from 'react';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { OrderedSet } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Toggle from 'react-toggle';
import {
  changeReportComment,
  changeReportForward,
  submitReport,
  changeReportType,
} from '../actions/reports';
import { refreshAccountTimeline } from '../../mastodon/actions/timelines';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { makeGetAccount } from '../../mastodon/selectors';
import StatusCheckBox from './report_check_box_container';
import Button from '../../mastodon/components/button';
import IconButton from '../../mastodon/components/icon_button';

const messages = defineMessages({
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
  placeholder: { id: 'pawoo.report.placeholder', defaultMessage: 'Additional comments (required for other cases)' },
  submit: { id: 'report.submit', defaultMessage: 'Submit' },
  reportTitle: { id: 'pawoo.report.select.title', defaultMessage: 'Please select the reason for reporting' },
  donotlike: { id: 'pawoo.report.select.donotlike', defaultMessage: 'I do not like it' },
  nsfw: { id: 'pawoo.report.select.nsfw', defaultMessage: 'Incorrect NSFW setting' },
  spam: { id: 'pawoo.report.select.spam', defaultMessage: 'Spam' },
  reproduction: { id: 'pawoo.report.select.reproduction', defaultMessage: 'Unauthorized reproduction' },
  prohibited: { id: 'pawoo.report.select.prohibited', defaultMessage: 'Prohibited act' },
  other: { id: 'pawoo.report.select.other', defaultMessage: 'Other' },
});

const makeMapStateToProps = () => {
  const getAccount = makeGetAccount();

  const mapStateToProps = state => {
    const accountId = state.getIn(['pawoo', 'reports', 'new', 'account_id']);

    return {
      isSubmitting: state.getIn(['pawoo', 'reports', 'new', 'isSubmitting']),
      account: getAccount(state, accountId),
      comment: state.getIn(['pawoo', 'reports', 'new', 'comment']),
      forward: state.getIn(['reports', 'new', 'forward']),
      reportType: state.getIn(['pawoo', 'reports', 'new', 'report_type']),
      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['pawoo', 'reports', 'new', 'status_ids'])),
    };
  };

  return mapStateToProps;
};

@injectIntl
@connect(makeMapStateToProps)
export default class ReportModal extends ImmutablePureComponent {

  static propTypes = {
    isSubmitting: PropTypes.bool,
    account: ImmutablePropTypes.map,
    statusIds: ImmutablePropTypes.orderedSet.isRequired,
    comment: PropTypes.string.isRequired,
    forward: PropTypes.bool,
    reportType: PropTypes.string,
    dispatch: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  options = ['donotlike', 'nsfw', 'spam', 'reproduction', 'prohibited', 'other'];

  handleCommentChange = e => {
    this.props.dispatch(changeReportComment(e.target.value));
  }

  handleForwardChange = e => {
    this.props.dispatch(changeReportForward(e.target.checked));
  }

  handleSubmit = () => {
    this.props.dispatch(submitReport());
  }

  componentDidMount () {
    this.props.dispatch(refreshAccountTimeline(this.props.account.get('id')));
  }

  componentWillReceiveProps (nextProps) {
    if (this.props.account !== nextProps.account && nextProps.account) {
      this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id')));
    }
  }

  onToggle = (e) => {
    this.props.dispatch(changeReportType(e.target.getAttribute('name')));
  }

  isSendable() {
    const { comment, reportType } = this.props;

    return this.options.includes(reportType) && (reportType !== 'other' || comment.length > 0);
  }

  render () {
    const { account, comment, reportType, intl, statusIds, isSubmitting, forward, onClose } = this.props;

    if (!account) {
      return null;
    }

    const domain = account.get('acct').split('@')[1];

    return (

          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
          <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: {account.get('acct')} }} />

{intl.formatMessage(messages.reportTitle)}

                {this.options.map(option => (

                      {intl.formatMessage(messages[option])}

                      <Toggle name={option} checked={option === reportType} onChange={this.onToggle} disabled={isSubmitting} />

                ))}

<FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' />

              <textarea
                className='setting-text light'
                placeholder={intl.formatMessage(messages.placeholder)}
                value={comment}
                onChange={this.handleCommentChange}
                disabled={isSubmitting}
              />

              {domain && (

<FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' />

                    <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
                    <FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />

              )}

            <Button disabled={isSubmitting} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />

              {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}

    );
  }

}
<commit_msg>Do not allow to send report if it is invalid<commit_after>import React from 'react';
import { connect } from 'react-redux';
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl';
import { OrderedSet } from 'immutable';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Toggle from 'react-toggle';
import {
  changeReportComment,
  changeReportForward,
  submitReport,
  changeReportType,
} from '../actions/reports';
import { refreshAccountTimeline } from '../../mastodon/actions/timelines';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { makeGetAccount } from '../../mastodon/selectors';
import StatusCheckBox from './report_check_box_container';
import Button from '../../mastodon/components/button';
import IconButton from '../../mastodon/components/icon_button';

const messages = defineMessages({
  close: { id: 'lightbox.close', defaultMessage: 'Close' },
  placeholder: { id: 'pawoo.report.placeholder', defaultMessage: 'Additional comments (required for other cases)' },
  submit: { id: 'report.submit', defaultMessage: 'Submit' },
  reportTitle: { id: 'pawoo.report.select.title', defaultMessage: 'Please select the reason for reporting' },
  donotlike: { id: 'pawoo.report.select.donotlike', defaultMessage: 'I do not like it' },
  nsfw: { id: 'pawoo.report.select.nsfw', defaultMessage: 'Incorrect NSFW setting' },
  spam: { id: 'pawoo.report.select.spam', defaultMessage: 'Spam' },
  reproduction: { id: 'pawoo.report.select.reproduction', defaultMessage: 'Unauthorized reproduction' },
  prohibited: { id: 'pawoo.report.select.prohibited', defaultMessage: 'Prohibited act' },
  other: { id: 'pawoo.report.select.other', defaultMessage: 'Other' },
});

const makeMapStateToProps = () => {
  const getAccount = makeGetAccount();

  const mapStateToProps = state => {
    const accountId = state.getIn(['pawoo', 'reports', 'new', 'account_id']);

    return {
      isSubmitting: state.getIn(['pawoo', 'reports', 'new', 'isSubmitting']),
      account: getAccount(state, accountId),
      comment: state.getIn(['pawoo', 'reports', 'new', 'comment']),
      forward: state.getIn(['reports', 'new', 'forward']),
      reportType: state.getIn(['pawoo', 'reports', 'new', 'report_type']),
      statusIds: OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['pawoo', 'reports', 'new', 'status_ids'])),
    };
  };

  return mapStateToProps;
};

@injectIntl
@connect(makeMapStateToProps)
export default class ReportModal extends ImmutablePureComponent {

  static propTypes = {
    isSubmitting: PropTypes.bool,
    account: ImmutablePropTypes.map,
    statusIds: ImmutablePropTypes.orderedSet.isRequired,
    comment: PropTypes.string.isRequired,
    forward: PropTypes.bool,
    reportType: PropTypes.string,
    dispatch: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  options = ['donotlike', 'nsfw', 'spam', 'reproduction', 'prohibited', 'other'];

  handleCommentChange = e => {
    this.props.dispatch(changeReportComment(e.target.value));
  }

  handleForwardChange = e => {
    this.props.dispatch(changeReportForward(e.target.checked));
  }

  handleSubmit = () => {
    this.props.dispatch(submitReport());
  }

  componentDidMount () {
    this.props.dispatch(refreshAccountTimeline(this.props.account.get('id')));
  }

  componentWillReceiveProps (nextProps) {
    if (this.props.account !== nextProps.account && nextProps.account) {
      this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id')));
    }
  }

  onToggle = (e) => {
    this.props.dispatch(changeReportType(e.target.getAttribute('name')));
  }

  isSendable() {
    const { comment, reportType } = this.props;

    return this.options.includes(reportType) && (reportType !== 'other' || comment.length > 0);
  }

  render () {
    const { account, comment, reportType, intl, statusIds, isSubmitting, forward, onClose } = this.props;

    if (!account) {
      return null;
    }

    const domain = account.get('acct').split('@')[1];

    return (

          <IconButton className='media-modal__close' title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} />
          <FormattedMessage id='report.target' defaultMessage='Report {target}' values={{ target: {account.get('acct')} }} />

{intl.formatMessage(messages.reportTitle)}

                {this.options.map(option => (

                      {intl.formatMessage(messages[option])}

                      <Toggle name={option} checked={option === reportType} onChange={this.onToggle} disabled={isSubmitting} />

                ))}

<FormattedMessage id='report.hint' defaultMessage='The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:' />

              <textarea
                className='setting-text light'
                placeholder={intl.formatMessage(messages.placeholder)}
                value={comment}
                onChange={this.handleCommentChange}
                disabled={isSubmitting}
              />

              {domain && (

<FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send an anonymized copy of the report there as well?' />

                    <Toggle id='report-forward' checked={forward} disabled={isSubmitting} onChange={this.handleForwardChange} />
                    <FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: domain }} />

              )}

            <Button disabled={isSubmitting || !this.isSendable()} text={intl.formatMessage(messages.submit)} onClick={this.handleSubmit} />

              {statusIds.map(statusId => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}

    );
  }

}
<|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 ==
 */

/* eslint-disable no-param-reassign */

import _ from 'lodash';
import util from 'util';

import annotate from '../../eventAnnotations';
import common from './common';
import simulations from '../../commonFunctions';

const isBrowser = typeof window !== 'undefined';
const debug = isBrowser ? require('bows')('InsuletDriver') : console.log;

/**
 * Creates a new "simulator" for Insulet OmniPod data.  The simulator has methods for events like
 *
 * cbg(), smbg(), basal(), bolus(), settingsChange(), etc.
 *
 * This simulator exists as an abstraction over the Tidepool APIs.  It was written to simplify the conversion
 * of static, "retrospective" audit logs from devices into events understood by the Tidepool platform.
 *
 * On the input side, you have events extracted from an Insulet .ibf file.  They should be delivered to the simulator
 * in time order.
 *
 * Once all input activities are collected, the simulator will have accumulated events that the Tidepool Platform
 * will understand into a local "events" array.  You can retrieve the events by calling `getEvents()`
 *
 * @param config
 * @returns {*}
 */
exports.make = (config = {}) => {
  const settings = config.settings || null;
  const events = [];

  let activationStatus = null;
  let currBasal = null;
  let currBolus = null;
  let currStatus = null;
  let currTimestamp = null;

  function setActivationStatus(status) {
    activationStatus = status;
  }

  function setCurrBasal(basal) {
    currBasal = basal;
  }

  function setCurrBolus(bolus) {
    currBolus = bolus;
  }

  function setCurrStatus(status) {
    currStatus = status;
  }

  function ensureTimestamp(e) {
    if (currTimestamp > e.time) {
      throw new Error(
        util.format(`Timestamps must be in order.  Current timestamp was ${currTimestamp}, but got ${e}`),
      );
    }
    currTimestamp = e.time;
    return e;
  }

  function simpleSimulate(e) {
    ensureTimestamp(e);
    events.push(e);
  }

  function fillInSuppressed(e) {
    let schedName = null;
    const suppressed = _.clone(e.suppressed);
    if (e.previous != null) {
      if (e.previous.deliveryType === 'scheduled') {
        schedName = e.previous.scheduleName;
      } else if (e.previous.deliveryType === 'temp') {
        if (e.previous.suppressed != null) {
          if (e.previous.suppressed.deliveryType === 'scheduled') {
            schedName = e.previous.suppressed.scheduleName;
          }
        }
      }
    }

    if (schedName != null) {
      e.suppressed = suppressed.with_scheduleName(schedName).done();
    } else {
      delete e.suppressed;
    }
  }

  return {
    alarm: (event) => {
      if (event.payload != null && event.payload.stopsDelivery === true) {
        if (event.status == null && event.index != null) {
          throw new Error('An Insulet alarm with a log index that has `stopsDelivery` in the payload must have a `status`.');
        }
      }
      simpleSimulate(event);
    },
    basal: (event) => {
      ensureTimestamp(event);
      if (currBasal != null) {
        // sometimes there can be duplicate suspend basals, so we return early
        // if we come across a suspend basal when we're already in one
        if (currBasal.deliveryType === 'suspend' && event.deliveryType === 'suspend') {
          return;
        }
        // completing a resume event from a new pod activation
        // see podActivation() below for more details
        if (currStatus != null && currStatus.status === 'suspended') {
          if (activationStatus != null && activationStatus.status === 'resumed') {
            const resume = activationStatus.with_previous(_.omit(currStatus, 'previous'))
              .with_reason({ resumed: 'manual' })
              .done();
            setCurrStatus(resume);
            events.push(resume);
            setActivationStatus(null);
          } else if (event.deliveryType !== 'suspend') {
            // if we're in a suspend basal, we need to leave the currStatus because
            // a resume is probably still impending
            // but if we've already moved on to a basal that's not deliveryType: suspend
            // then we need to wipe the slate clean
            setCurrStatus(null);
          }
        }
        if (!currBasal.isAssigned('duration')) {
          currBasal.with_duration(Date.parse(event.time) - Date.parse(currBasal.time));
        }
        if (currBasal.isAssigned('suppressed')) {
          fillInSuppressed(currBasal);
        }
        currBasal = currBasal.done();
        event.previous = _.omit(currBasal, 'previous');
        events.push(currBasal);
      } else if (activationStatus != null && activationStatus.status === 'resumed') {
        // at the very beginning of a file (which === when currBasal is null) it is common
        // to see a pod activation and then a basal; the former will end up as a `deviceEvent`
        // resume without a `previous` but it's technically accurate, so we upload it anyway
        let initialResume;
        // we don't really expect this to happen, but just in case...
        if (currStatus != null && currStatus.status === 'suspended') {
          initialResume = activationStatus.with_previous(_.omit(currStatus, 'previous'))
            .with_reason({ resumed: 'manual' })
            .done();
          setCurrStatus(initialResume);
          events.push(initialResume);
        } else {
        // this is the more common case, in which case we finish building a resume
        // that won't be connected with a suspend, kinda pointless, but accurate
          initialResume = activationStatus.with_reason({ resumed: 'manual' })
            .done();
          setCurrStatus(initialResume);
          events.push(initialResume);
        }
        setActivationStatus(null);
      }
      setCurrBasal(event);
    },
    bolus: (event) => {
      simpleSimulate(event);
      setCurrBolus(event);
    },
    /*
     * When an OmniPod users cancels a bolus partway through delivery, the data includes
     * a bolus termination event, and this is the only way for us to access the information
     * that the bolus volume intially programmed and the bolus volume actually delivered
     * were not the same.
     *
     * The logic probably looks a little funny here: we add the `missedInsulin` from the bolus
     * termination to the bolus volume reported on the bolus event to obtain the bolus volume
     * that the user initially programmed. This is because an Insulet bolus record always
     * reports the volume of bolus that was actually delivered, not the bolus that was programmed.
     * (Same for duration on extended bolus (components).)
     */
    bolusTermination: (event) => {
      if (currBolus != null) {
        if (currBolus.subType === 'normal') {
          currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2);
        } else if (event.durationLeft > 0) {
          currBolus.expectedExtended = common.fixFloatingPoint(currBolus.extended + event.missedInsulin, 2);
          currBolus.expectedDuration = currBolus.duration + event.durationLeft;
        } else {
          currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2);
        }
      } else {
        debug('Cannot find bolus to modify given bolus termination: [%j]. PDM was likely reset.', event);
      }
    },
    changeDeviceTime: (event) => {
      simpleSimulate(event);
    },
    changeReservoir: (event) => {
      if (event.status == null) {
        throw new Error('An Insulet `reservoirChange` event must have a `status`.');
      }
      simpleSimulate(event);
    },
    /*
     * We simulate the final basal in an Insulet .ibf file as a special case, to keep the logic
     * of basal() above cleaner. This basal is special because we don't have a following basal
     * to use to determine the duration. Instead we look up the basal against the settings at
     * the time of upload and try to determine a duration for the basal based on this information.
     */
    finalBasal: () => {
      if (currBasal != null) {
        if (currBasal.deliveryType !== 'scheduled') {
          if (currBasal.deliveryType === 'temp') {
            if (currBasal.isAssigned('suppressed')) {
              fillInSuppressed(currBasal);
            }
            currBasal = currBasal.done();
          } else if (!currBasal.isAssigned('duration')) {
            currBasal.duration = 0;
            annotate.annotateEvent(currBasal, 'basal/unknown-duration');
            currBasal = currBasal.done();
          } else {
            currBasal = currBasal.done();
          }
        } else if (settings != null) {
          currBasal = simulations.finalScheduledBasal(currBasal, settings, 'insulet');
        } else {
          currBasal.with_duration(0);
          annotate.annotateEvent(currBasal, 'basal/unknown-duration');
          currBasal = currBasal.done();
        }
        events.push(currBasal);
      }
    },
    /*
     * Pod activations are not *quite* resume events (because further user intervention is
     * required before insulin delivery resumes - namely, confirming that cannula insertion
     * was successful). So we build activations as resumes, save them in `activationStatus`
     * and then complete them as resumes upon receipt of the next `basal` event.
     */
    podActivation: (event) => {
      ensureTimestamp(event);
      setActivationStatus(event);
    },
    resume: (event) => {
      ensureTimestamp(event);
      if (currStatus != null && currStatus.status === 'suspended') {
        event = event.with_previous(_.omit(currStatus, 'previous'));
      }
      event = event.done();
      setCurrStatus(event);
      events.push(event);
    },
    pumpSettings: (event) => {
      simpleSimulate(event);
    },
    smbg: (event) => {
      simpleSimulate(event);
    },
    suspend: (event) => {
      // suspends in a series are pretty common - e.g., when an alarm that implies
      // a stoppage of delivery produces a suspend and then we also get something
      // like a pod deactivation immediately following
      // if we're already in a suspended state, we just return early to maintain that state
      if (currStatus != null && currStatus.status === 'suspended') {
        return;
      }
      ensureTimestamp(event);
      // there can be stray activation events that hang around
      // i.e., when there was a PDM error and then a date & time change
      // they are definitely no longer relevant if we come across an actual suspend
      // so we reset back to null
      if (activationStatus != null && activationStatus.status === 'resumed') {
        setActivationStatus(null);
      }
      setCurrStatus(event);
      events.push(event);
    },
    wizard: (event) => {
      simpleSimulate(event);
    },
    getEvents: () => {
      function filterOutZeroBoluses() {
        return _.filter(events, (event) => {
          // we include the index on all objects to be able to sort accurately in
          // pump-event order despite date & time settings changes, but it's not
          // part of our data model, so we delete before uploading
          delete event.index;
          if (event.type === 'bolus') {
            if (event.normal === 0 && !event.expectedNormal && !event.carbInput) {
              return false;
            }
            delete event.carbInput;
            return true;
          }

          if (event.type === 'wizard') {
            const bolus = event.bolus || null;
            if (bolus != null) {
              if (bolus.normal === 0 && !bolus.expectedNormal && !event.carbInput) {
                return false;
              }
              return true;
            }
          }
          return true;
        });
      }
      // because we have to wait for the *next* basal to determine the duration of a current
      // basal, basal events get added to `events` out of order wrt other events
      // (although within their own type all the basals are always in order)
      // end result: we have to sort events again before we try to upload them
      const orderedEvents = _.sortBy(filterOutZeroBoluses(), (e) => e.time);

      return orderedEvents;
    },
  };
};
<commit_msg>as temp basal percent can be 0, we use previous for suppressed rate<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 ==
 */

/* eslint-disable no-param-reassign */

import _ from 'lodash';
import util from 'util';

import annotate from '../../eventAnnotations';
import common from './common';
import simulations from '../../commonFunctions';

const isBrowser = typeof window !== 'undefined';
const debug = isBrowser ? require('bows')('InsuletDriver') : console.log;

/**
 * Creates a new "simulator" for Insulet OmniPod data.  The simulator has methods for events like
 *
 * cbg(), smbg(), basal(), bolus(), settingsChange(), etc.
 *
 * This simulator exists as an abstraction over the Tidepool APIs.  It was written to simplify the conversion
 * of static, "retrospective" audit logs from devices into events understood by the Tidepool platform.
 *
 * On the input side, you have events extracted from an Insulet .ibf file.  They should be delivered to the simulator
 * in time order.
 *
 * Once all input activities are collected, the simulator will have accumulated events that the Tidepool Platform
 * will understand into a local "events" array.  You can retrieve the events by calling `getEvents()`
 *
 * @param config
 * @returns {*}
 */
exports.make = (config = {}) => {
  const settings = config.settings || null;
  const events = [];

  let activationStatus = null;
  let currBasal = null;
  let currBolus = null;
  let currStatus = null;
  let currTimestamp = null;

  function setActivationStatus(status) {
    activationStatus = status;
  }

  function setCurrBasal(basal) {
    currBasal = basal;
  }

  function setCurrBolus(bolus) {
    currBolus = bolus;
  }

  function setCurrStatus(status) {
    currStatus = status;
  }

  function ensureTimestamp(e) {
    if (currTimestamp > e.time) {
      throw new Error(
        util.format(`Timestamps must be in order.  Current timestamp was ${currTimestamp}, but got ${e}`),
      );
    }
    currTimestamp = e.time;
    return e;
  }

  function simpleSimulate(e) {
    ensureTimestamp(e);
    events.push(e);
  }

  function fillInSuppressed(e) {
    let schedName = null;
    let rate = null;
    const suppressed = _.clone(e.suppressed);
    if (e.previous != null) {
      if (e.previous.deliveryType === 'scheduled') {
        schedName = e.previous.scheduleName;
        rate = e.previous.rate;
      } else if (e.previous.deliveryType === 'temp') {
        if (e.previous.suppressed != null) {
          if (e.previous.suppressed.deliveryType === 'scheduled') {
            schedName = e.previous.suppressed.scheduleName;
            rate = e.previous.suppressed.rate;
          }
        }
      }
    }

    if (schedName != null) {
      e.suppressed = suppressed.with_scheduleName(schedName)
        .with_rate(rate)
        .done();
    } else {
      delete e.suppressed;
    }
  }

  return {
    alarm: (event) => {
      if (event.payload != null && event.payload.stopsDelivery === true) {
        if (event.status == null && event.index != null) {
          throw new Error('An Insulet alarm with a log index that has `stopsDelivery` in the payload must have a `status`.');
        }
      }
      simpleSimulate(event);
    },
    basal: (event) => {
      ensureTimestamp(event);
      if (currBasal != null) {
        // sometimes there can be duplicate suspend basals, so we return early
        // if we come across a suspend basal when we're already in one
        if (currBasal.deliveryType === 'suspend' && event.deliveryType === 'suspend') {
          return;
        }
        // completing a resume event from a new pod activation
        // see podActivation() below for more details
        if (currStatus != null && currStatus.status === 'suspended') {
          if (activationStatus != null && activationStatus.status === 'resumed') {
            const resume = activationStatus.with_previous(_.omit(currStatus, 'previous'))
              .with_reason({ resumed: 'manual' })
              .done();
            setCurrStatus(resume);
            events.push(resume);
            setActivationStatus(null);
          } else if (event.deliveryType !== 'suspend') {
            // if we're in a suspend basal, we need to leave the currStatus because
            // a resume is probably still impending
            // but if we've already moved on to a basal that's not deliveryType: suspend
            // then we need to wipe the slate clean
            setCurrStatus(null);
          }
        }
        if (!currBasal.isAssigned('duration')) {
          currBasal.with_duration(Date.parse(event.time) - Date.parse(currBasal.time));
        }
        if (currBasal.isAssigned('suppressed')) {
          fillInSuppressed(currBasal);
        }
        currBasal = currBasal.done();
        event.previous = _.omit(currBasal, 'previous');
        events.push(currBasal);
      } else if (activationStatus != null && activationStatus.status === 'resumed') {
        // at the very beginning of a file (which === when currBasal is null) it is common
        // to see a pod activation and then a basal; the former will end up as a `deviceEvent`
        // resume without a `previous` but it's technically accurate, so we upload it anyway
        let initialResume;
        // we don't really expect this to happen, but just in case...
        if (currStatus != null && currStatus.status === 'suspended') {
          initialResume = activationStatus.with_previous(_.omit(currStatus, 'previous'))
            .with_reason({ resumed: 'manual' })
            .done();
          setCurrStatus(initialResume);
          events.push(initialResume);
        } else {
        // this is the more common case, in which case we finish building a resume
        // that won't be connected with a suspend, kinda pointless, but accurate
          initialResume = activationStatus.with_reason({ resumed: 'manual' })
            .done();
          setCurrStatus(initialResume);
          events.push(initialResume);
        }
        setActivationStatus(null);
      }
      setCurrBasal(event);
    },
    bolus: (event) => {
      simpleSimulate(event);
      setCurrBolus(event);
    },
    /*
     * When an OmniPod users cancels a bolus partway through delivery, the data includes
     * a bolus termination event, and this is the only way for us to access the information
     * that the bolus volume intially programmed and the bolus volume actually delivered
     * were not the same.
     *
     * The logic probably looks a little funny here: we add the `missedInsulin` from the bolus
     * termination to the bolus volume reported on the bolus event to obtain the bolus volume
     * that the user initially programmed. This is because an Insulet bolus record always
     * reports the volume of bolus that was actually delivered, not the bolus that was programmed.
     * (Same for duration on extended bolus (components).)
     */
    bolusTermination: (event) => {
      if (currBolus != null) {
        if (currBolus.subType === 'normal') {
          currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2);
        } else if (event.durationLeft > 0) {
          currBolus.expectedExtended = common.fixFloatingPoint(currBolus.extended + event.missedInsulin, 2);
          currBolus.expectedDuration = currBolus.duration + event.durationLeft;
        } else {
          currBolus.expectedNormal = common.fixFloatingPoint(currBolus.normal + event.missedInsulin, 2);
        }
      } else {
        debug('Cannot find bolus to modify given bolus termination: [%j]. PDM was likely reset.', event);
      }
    },
    changeDeviceTime: (event) => {
      simpleSimulate(event);
    },
    changeReservoir: (event) => {
      if (event.status == null) {
        throw new Error('An Insulet `reservoirChange` event must have a `status`.');
      }
      simpleSimulate(event);
    },
    /*
     * We simulate the final basal in an Insulet .ibf file as a special case, to keep the logic
     * of basal() above cleaner. This basal is special because we don't have a following basal
     * to use to determine the duration. Instead we look up the basal against the settings at
     * the time of upload and try to determine a duration for the basal based on this information.
     */
    finalBasal: () => {
      if (currBasal != null) {
        if (currBasal.deliveryType !== 'scheduled') {
          if (currBasal.deliveryType === 'temp') {
            if (currBasal.isAssigned('suppressed')) {
              fillInSuppressed(currBasal);
            }
            currBasal = currBasal.done();
          } else if (!currBasal.isAssigned('duration')) {
            currBasal.duration = 0;
            annotate.annotateEvent(currBasal, 'basal/unknown-duration');
            currBasal = currBasal.done();
          } else {
            currBasal = currBasal.done();
          }
        } else if (settings != null) {
          currBasal = simulations.finalScheduledBasal(currBasal, settings, 'insulet');
        } else {
          currBasal.with_duration(0);
          annotate.annotateEvent(currBasal, 'basal/unknown-duration');
          currBasal = currBasal.done();
        }
        events.push(currBasal);
      }
    },
    /*
     * Pod activations are not *quite* resume events (because further user intervention is
     * required before insulin delivery resumes - namely, confirming that cannula insertion
     * was successful). So we build activations as resumes, save them in `activationStatus`
     * and then complete them as resumes upon receipt of the next `basal` event.
     */
    podActivation: (event) => {
      ensureTimestamp(event);
      setActivationStatus(event);
    },
    resume: (event) => {
      ensureTimestamp(event);
      if (currStatus != null && currStatus.status === 'suspended') {
        event = event.with_previous(_.omit(currStatus, 'previous'));
      }
      event = event.done();
      setCurrStatus(event);
      events.push(event);
    },
    pumpSettings: (event) => {
      simpleSimulate(event);
    },
    smbg: (event) => {
      simpleSimulate(event);
    },
    suspend: (event) => {
      // suspends in a series are pretty common - e.g., when an alarm that implies
      // a stoppage of delivery produces a suspend and then we also get something
      // like a pod deactivation immediately following
      // if we're already in a suspended state, we just return early to maintain that state
      if (currStatus != null && currStatus.status === 'suspended') {
        return;
      }
      ensureTimestamp(event);
      // there can be stray activation events that hang around
      // i.e., when there was a PDM error and then a date & time change
      // they are definitely no longer relevant if we come across an actual suspend
      // so we reset back to null
      if (activationStatus != null && activationStatus.status === 'resumed') {
        setActivationStatus(null);
      }
      setCurrStatus(event);
      events.push(event);
    },
    wizard: (event) => {
      simpleSimulate(event);
    },
    getEvents: () => {
      function filterOutZeroBoluses() {
        return _.filter(events, (event) => {
          // we include the index on all objects to be able to sort accurately in
          // pump-event order despite date & time settings changes, but it's not
          // part of our data model, so we delete before uploading
          delete event.index;
          if (event.type === 'bolus') {
            if (event.normal === 0 && !event.expectedNormal && !event.carbInput) {
              return false;
            }
            delete event.carbInput;
            return true;
          }

          if (event.type === 'wizard') {
            const bolus = event.bolus || null;
            if (bolus != null) {
              if (bolus.normal === 0 && !bolus.expectedNormal && !event.carbInput) {
                return false;
              }
              return true;
            }
          }
          return true;
        });
      }
      // because we have to wait for the *next* basal to determine the duration of a current
      // basal, basal events get added to `events` out of order wrt other events
      // (although within their own type all the basals are always in order)
      // end result: we have to sort events again before we try to upload them
      const orderedEvents = _.sortBy(filterOutZeroBoluses(), (e) => e.time);

      return orderedEvents;
    },
  };
};
<|endoftext|>


<commit_before>const Batch = require('../Batch');

describe('Batch', function () {
  beforeEach(function () {
    this.batch = new Batch();
  });

  describe('#add', function () {

    it('should not throw an error', function () {
      expect(() => {
        this.batch.add({
          action: 'test'
        });
      }).not.toThrow();
    });

  });

  describe('#reduce', function () {

    it('should iterate correctly over a batch with 1 item', function () {
      this.batch.add({action: 'a'});
      let actions = this.batch.reduce(function (actions, item) {
        actions.push(item.action);
        return actions;
      }, []);

      expect(actions).toEqual(['a']);
    });

    it('should iterate correctly over a batch with 3 item', function () {
      this.batch.add({action: 'a'});
      this.batch.add({action: 'b'});
      this.batch.add({action: 'c'});
      let actions = this.batch.reduce(function (actions, item) {
        actions.push(item.action);
        return actions;
      }, []);

      expect(actions).toEqual(['a', 'b', 'c']);
    });

  });

});
<commit_msg>fixing batch tests<commit_after>const Batch = require('../Batch');

describe('Batch', function () {
  beforeEach(function () {
    this.batch = new Batch();
  });

  describe('#add', function () {

    it('should not throw an error', function () {
      expect(() => {
        this.batch.add({
          action: 'test'
        });
      }).not.toThrow();
    });

  });

  describe('#reduce', function () {

    it('should iterate correctly over a batch with 1 item', function () {
      this.batch.add({action: 'a'});
      let actions = this.batch.reduce(function (actions, item) {
        actions.push(item.action);
        return actions;
      }, []);

      expect(actions).toEqual(['INIT', 'a']);
    });

    it('should iterate correctly over a batch with 3 item', function () {
      this.batch.add({action: 'a'});
      this.batch.add({action: 'b'});
      this.batch.add({action: 'c'});
      let actions = this.batch.reduce(function (actions, item) {
        actions.push(item.action);
        return actions;
      }, []);

      expect(actions).toEqual(['INIT', 'a', 'b', 'c']);
    });

  });

});
<|endoftext|>