<commit_before>module.exports.initiate_combat = _initiate_combat;
//TODO: Add strings for sanity damage
//TODO: Implement use of attributes besides damage in combat.
// ^^ this should be done in the npc/player src files
//TODO: Implement use of combat stance, etc. for strategery.
//FIXME: Combat ends when you die but you get double prompted.

var LevelUtil = require('./levels')
  .LevelUtil;
var CommandUtil = require('./command_util')
  .CommandUtil;
var util = require('util');
var statusUtils = require('./status');

function _initiate_combat(l10n, npc, player, room, npcs, players, rooms, callback) {
  var locale = player.getLocale();
  player.setInCombat(npc);
  npc.setInCombat(player.getName());

  player.sayL10n(l10n, 'ATTACK', npc.getShortDesc(locale));

  var p_locations = [
    'legs',
    'fists',
    'torso',
    'hands', 'head'
    ];

  var p = {
    isPlayer: true,
    name: player.getName(),
    speed: player.getAttackSpeed(),
    weapon: player.getEquipped('wield', true),
    locations: p_locations,
    target: 'body',
  };

  var n = {
    name: npc.getShortDesc(locale),
    speed: npc.getAttackSpeed(),
    weapon: npc.getAttack(locale),
    target: npc.getAttribute('target'),
  };

  util.log("Combat begins between " + p.name + " and " + n.name);
  util.log("Weapons are " + p.weapon.getShortDesc('en') + ' and ' + n.weapon);
  util.log("Speeds are " + p.speed + ' vs. ' + n.speed);

  var player_combat = combatRound.bind(null, player, npc, p, n);
  var npc_combat = combatRound.bind(null, npc, player, n, p);

  p.attackRound = player_combat;
  n.attackRound = npc_combat;

  setTimeout(npc_combat, n.speed);
  setTimeout(player_combat, p.speed);

  function combatRound(attacker, defender, a, d) {

    if (!defender.isInCombat() || !attacker.isInCombat())
      return;

    var damage = attacker.getDamage();
    var defender_sanity = defender.getAttribute('sanity');
    var sanityDamage = a.isPlayer ? 0 : attacker.getSanityDamage();
    var hitLocation = d.isPlayer ? decideHitLocation(d.locations, a.target) : 'body';

    if (!damage) {

      if (d.weapon && typeof d.weapon == 'Object')
        d.weapon.emit('parry', defender);

      if (a.isPlayer)
        player.sayL10n(l10n, 'PLAYER_MISS', n.name, damage);

      else
        player.sayL10n(l10n, 'NPC_MISS', a.name);

      broadcastExceptPlayer(
        '<bold>' + a.name + ' attacks ' + d.name +
        ' and misses!' + '</bold>');
      util.log(a.name + ' misses ' + d.name);

    } else {

      damage = defender.damage(calcRawDamage(damage, defender.getAttribute('health')), hitLocation);
      var damageStr = getDamageString(damage, defender.getAttribute('health'));

      if (a.weapon && typeof a.weapon == 'Object')
        a.weapon.emit('hit', player);

      if (d.isPlayer)
        player.sayL10n(l10n, 'DAMAGE_TAKEN', a.name, damageStr, a.weapon);

      else player.sayL10n(l10n, 'DAMAGE_DONE', d.name, damageStr);

      broadcastExceptPlayer('<bold><red>' + a.name + ' attacks ' + d.name +
        ' and ' + damageStr + ' them!' + '</red></bold>');

    }

    if (sanityDamage) {
      sanityDamage = calcRawDamage(sanityDamage, defender_sanity);
      defender.setAttribute('sanity', Math.max(defender_sanity - sanityDamage, 0));
    }

    if (defender.getAttribute('health') <= damage) {
      defender.setAttribute('health', 1);
      defender.setAttribute('sanity', 1);
      return combat_end(a.isPlayer);
    }

    player.combatPrompt({
      target_condition: statusUtils.getHealthText(
        npc.getAttribute('max_health'),
        player, npc)(npc.getAttribute('health')),
      player_condition: statusUtils.getHealthText(
        player.getAttribute('max_health'),
        player, false)(player.getAttribute('health'))
    });

    broadcastToArea("The sounds of a nearby struggle fill the air.");

    setTimeout(a.attackRound, a.speed);
  }

  function decideHitLocation(locations, target) {
    if (CommandUtil.isCoinFlip()) {
      return target;
    } else return CommandUtil.getRandomFromArr(locations);
  }

  function calcRawDamage(damage, attr) {
    var range = damage.max - damage.min;
    with(Math) {
      return min(
        attr,
        damage.min + max(
          0,
          floor(random() * (range))
        )
      );
    }
  }

  function getDamageString(damage, health) {
    var percentage = Math.round((damage / health) * 100);

    var damageStrings = {
      1: 'annoys',
      3: 'scratches',
      8: 'grazes',
      20: 'hits',
      35: 'wounds',
      85: 'maims',
    };

    for (var cutoff in damageStrings) {
      if (percentage <= cutoff) {
        return damageStrings[cutoff];
      }
    }
    return 'crushes';
  }

  function combat_end(success) {

    util.log("*** Combat Over ***")

    player.setInCombat(false);
    npc.setInCombat(false);

    if (success) {

      player.emit('regen');
      room.removeNpc(npc.getUuid());
      npcs.destroy(npc);
      player.sayL10n(l10n, 'WIN', npc.getShortDesc(locale));
      broadcastExceptPlayer('<bold>' + npc.getShortDesc(locale) +
        ' dies.</bold>');

      // hand out experience
      var exp = npc.getAttribute('experience') !== false ?
        npc.getAttribute('experience') : LevelUtil.mobExp(npc.getAttribute('level'));
      util.log("Player wins, exp gain: ", exp);
      player.emit('experience', exp);
    } else {
      util.log("Player death: ", player.getName());
      player.sayL10n(l10n, 'LOSE', npc.getShortDesc(locale));
      player.emit('die');

      broadcastExceptPlayer(player.getName() +
        ' collapses to the ground, life fleeing their body before your eyes.'
      );

      //TODO: consider doing sanity damage to all other players in the room.
      broadcastExceptPlayer('<blue>A horrible feeling gnaws at the pit of your stomach.</blue>');
      npc.setAttribute('health', npc.getAttribute('max_health'));

      broadcastToArea('The gurgles of a dying ' +
        statusUtils.getGenderNoun(player) +
        ' echo from nearby.'
      );
    }
    player.prompt();
    callback(success);
  }

  //TODO: More candidates for utilification, I suppose.

  function broadcastExceptPlayer(msg) {
    players.eachExcept(player, function(p) {
      if (p.getLocation() === player.getLocation()) {
        p.say(msg);
        p.prompt();
      }
    });
  }

  function broadcastToArea(msg) {
    players.eachExcept(player, function(p) {
      if (rooms.getAt(p.getLocation())
        .getArea() === rooms.getAt(player.getLocation())
        .getArea()) {
        p.say(msg);
        p.prompt();
      }
    });
  }
}
<commit_msg>Fix player locations<commit_after>module.exports.initiate_combat = _initiate_combat;
//TODO: Add strings for sanity damage
//TODO: Implement use of attributes besides damage in combat.
// ^^ this should be done in the npc/player src files
//TODO: Implement use of combat stance, etc. for strategery.
//FIXME: Combat ends when you die but you get double prompted.

var LevelUtil = require('./levels')
  .LevelUtil;
var CommandUtil = require('./command_util')
  .CommandUtil;
var util = require('util');
var statusUtils = require('./status');

function _initiate_combat(l10n, npc, player, room, npcs, players, rooms, callback) {
  var locale = player.getLocale();
  player.setInCombat(npc);
  npc.setInCombat(player.getName());

  player.sayL10n(l10n, 'ATTACK', npc.getShortDesc(locale));

  var p_locations = [
    'legs',
    'feet'
    'torso',
    'hands',
    'head'
    ];

  var p = {
    isPlayer: true,
    name: player.getName(),
    speed: player.getAttackSpeed(),
    weapon: player.getEquipped('wield', true),
    locations: p_locations,
    target: 'body',
  };

  var n = {
    name: npc.getShortDesc(locale),
    speed: npc.getAttackSpeed(),
    weapon: npc.getAttack(locale),
    target: npc.getAttribute('target'),
  };

  util.log("Combat begins between " + p.name + " and " + n.name);
  util.log("Weapons are " + p.weapon.getShortDesc('en') + ' and ' + n.weapon);
  util.log("Speeds are " + p.speed + ' vs. ' + n.speed);

  var player_combat = combatRound.bind(null, player, npc, p, n);
  var npc_combat = combatRound.bind(null, npc, player, n, p);

  p.attackRound = player_combat;
  n.attackRound = npc_combat;

  setTimeout(npc_combat, n.speed);
  setTimeout(player_combat, p.speed);

  function combatRound(attacker, defender, a, d) {

    if (!defender.isInCombat() || !attacker.isInCombat())
      return;

    var damage = attacker.getDamage();
    var defender_sanity = defender.getAttribute('sanity');
    var sanityDamage = a.isPlayer ? 0 : attacker.getSanityDamage();
    var hitLocation = d.isPlayer ? decideHitLocation(d.locations, a.target) : 'body';

    if (!damage) {

      if (d.weapon && typeof d.weapon == 'Object')
        d.weapon.emit('parry', defender);

      if (a.isPlayer)
        player.sayL10n(l10n, 'PLAYER_MISS', n.name, damage);

      else
        player.sayL10n(l10n, 'NPC_MISS', a.name);

      broadcastExceptPlayer(
        '<bold>' + a.name + ' attacks ' + d.name +
        ' and misses!' + '</bold>');
      util.log(a.name + ' misses ' + d.name);

    } else {

      damage = defender.damage(calcRawDamage(damage, defender.getAttribute('health')), hitLocation);
      var damageStr = getDamageString(damage, defender.getAttribute('health'));

      if (a.weapon && typeof a.weapon == 'Object')
        a.weapon.emit('hit', player);

      if (d.isPlayer)
        player.sayL10n(l10n, 'DAMAGE_TAKEN', a.name, damageStr, a.weapon);

      else player.sayL10n(l10n, 'DAMAGE_DONE', d.name, damageStr);

      broadcastExceptPlayer('<bold><red>' + a.name + ' attacks ' + d.name +
        ' and ' + damageStr + ' them!' + '</red></bold>');

    }

    if (sanityDamage) {
      sanityDamage = calcRawDamage(sanityDamage, defender_sanity);
      defender.setAttribute('sanity', Math.max(defender_sanity - sanityDamage, 0));
    }

    if (defender.getAttribute('health') <= damage) {
      defender.setAttribute('health', 1);
      defender.setAttribute('sanity', 1);
      return combat_end(a.isPlayer);
    }

    player.combatPrompt({
      target_condition: statusUtils.getHealthText(
        npc.getAttribute('max_health'),
        player, npc)(npc.getAttribute('health')),
      player_condition: statusUtils.getHealthText(
        player.getAttribute('max_health'),
        player, false)(player.getAttribute('health'))
    });

    broadcastToArea("The sounds of a nearby struggle fill the air.");

    setTimeout(a.attackRound, a.speed);
  }

  function decideHitLocation(locations, target) {
    if (CommandUtil.isCoinFlip()) {
      return target;
    } else return CommandUtil.getRandomFromArr(locations);
  }

  function calcRawDamage(damage, attr) {
    var range = damage.max - damage.min;
    with(Math) {
      return min(
        attr,
        damage.min + max(
          0,
          floor(random() * (range))
        )
      );
    }
  }

  function getDamageString(damage, health) {
    var percentage = Math.round((damage / health) * 100);

    var damageStrings = {
      1: 'annoys',
      3: 'scratches',
      8: 'grazes',
      20: 'hits',
      35: 'wounds',
      85: 'maims',
    };

    for (var cutoff in damageStrings) {
      if (percentage <= cutoff) {
        return damageStrings[cutoff];
      }
    }
    return 'crushes';
  }

  function combat_end(success) {

    util.log("*** Combat Over ***")

    player.setInCombat(false);
    npc.setInCombat(false);

    if (success) {

      player.emit('regen');
      room.removeNpc(npc.getUuid());
      npcs.destroy(npc);
      player.sayL10n(l10n, 'WIN', npc.getShortDesc(locale));
      broadcastExceptPlayer('<bold>' + npc.getShortDesc(locale) +
        ' dies.</bold>');

      // hand out experience
      var exp = npc.getAttribute('experience') !== false ?
        npc.getAttribute('experience') : LevelUtil.mobExp(npc.getAttribute('level'));
      util.log("Player wins, exp gain: ", exp);
      player.emit('experience', exp);
    } else {
      util.log("Player death: ", player.getName());
      player.sayL10n(l10n, 'LOSE', npc.getShortDesc(locale));
      player.emit('die');

      broadcastExceptPlayer(player.getName() +
        ' collapses to the ground, life fleeing their body before your eyes.'
      );

      //TODO: consider doing sanity damage to all other players in the room.
      broadcastExceptPlayer('<blue>A horrible feeling gnaws at the pit of your stomach.</blue>');
      npc.setAttribute('health', npc.getAttribute('max_health'));

      broadcastToArea('The gurgles of a dying ' +
        statusUtils.getGenderNoun(player) +
        ' echo from nearby.'
      );
    }
    player.prompt();
    callback(success);
  }

  //TODO: More candidates for utilification, I suppose.

  function broadcastExceptPlayer(msg) {
    players.eachExcept(player, function(p) {
      if (p.getLocation() === player.getLocation()) {
        p.say(msg);
        p.prompt();
      }
    });
  }

  function broadcastToArea(msg) {
    players.eachExcept(player, function(p) {
      if (rooms.getAt(p.getLocation())
        .getArea() === rooms.getAt(player.getLocation())
        .getArea()) {
        p.say(msg);
        p.prompt();
      }
    });
  }
}
<|endoftext|>


<commit_before>// @flow
/**
 * All About Olaf
 * Streaming Media page
 */

import React from 'react'
import {StyleSheet, SectionList} from 'react-native'

import * as c from '../../components/colors'
import {ListSeparator, ListSectionHeader} from '../../components/list'
import LoadingView from '../../components/loading'
import {NoticeView} from '../../components/notice'
import {TabBarIcon} from '../../components/tabbar-icon'
import {StreamRow} from './row'
import toPairs from 'lodash/toPairs'
import groupBy from 'lodash/groupBy'
import moment from 'moment-timezone'
import qs from 'querystring'
import {toLaxTitleCase as titleCase} from 'titlecase'
import type {StreamType} from './types'
import delay from 'delay'

const CENTRAL_TZ = 'America/Winnipeg'
const url = 'https://www.stolaf.edu/multimedia/api/collection'

const styles = StyleSheet.create({
  listContainer: {
    backgroundColor: c.white,
  },
})

export class StreamListView extends React.PureComponent {
  static navigationOptions = {
    tabBarLabel: 'Streaming',
    tabBarIcon: TabBarIcon('recording'),
  }

  state: {
    error: ?Error,
    loaded: boolean,
    noStreams: boolean,
    refreshing: boolean,
    streams: Array<{title: string, data: Array<StreamType>}>,
  } = {
    error: null,
    loaded: false,
    noStreams: false,
    refreshing: false,
    streams: [],
  }

  componentWillMount() {
    this.getStreams()
  }

  refresh = async () => {
    let start = Date.now()
    this.setState(() => ({refreshing: true}))

    await this.getStreams()

    // wait 0.5 seconds – if we let it go at normal speed, it feels broken.
    const elapsed = Date.now() - start
    if (elapsed < 500) {
      await delay(500 - elapsed)
    }

    this.setState(() => ({refreshing: false}))
  }

  getStreams = async (date: moment = moment.tz(CENTRAL_TZ)) => {
    try {
      const dateFrom = date.format('YYYY-MM-DD')
      const dateTo = date.add(1, 'month').format('YYYY-MM-DD')

      let params = {
        class: 'upcoming',
        sort: 'ascending',
        // eslint-disable-next-line camelcase
        date_from: dateFrom,
        // eslint-disable-next-line camelcase
        date_to: dateTo,
      }

      const streamsAPI = `${url}?${qs.stringify(params)}`
      const data = await fetchJson(streamsAPI)
      const streams = data.results

      if (streams.length > 1) {
        this.setState(() => ({noStreams: true}))
      }

      // force title-case on the stream types, to prevent not-actually-duplicate headings
      const processed = streams
        .filter(stream => stream.category !== 'athletics')
        .map(stream => {
          const date = moment(stream.starttime, 'YYYY-MM-DD HH:mm')
          return {
            ...stream,
            category: titleCase(stream.category),
            date: date,
            $groupBy: date.format('dddd, MMMM Do'),
          }
        })

      const grouped = groupBy(processed, j => j.$groupBy)
      const mapped = toPairs(grouped).map(([title, data]) => ({title, data}))

      this.setState(() => ({
        error: null,
        loaded: true,
        streams: mapped,
      }))
    } catch (error) {
      this.setState(() => ({error: error.message}))
      console.warn(error)
    }
  }

  keyExtractor = (item: StreamType) => item.eid

  renderSectionHeader = ({section: {title}}: any) => (
    <ListSectionHeader title={title} />
  )

  renderItem = ({item}: {item: StreamType}) => <StreamRow stream={item} />

  render() {
    if (!this.state.loaded) {
      return <LoadingView />
    }

    if (this.state.error) {
      return <NoticeView text={'Error: ' + this.state.error.message} />
    }

    return (
      <SectionList
        ListEmptyComponent={<NoticeView text="No Streams" />}
        renderSectionHeader={this.renderSectionHeader}
        sections={(this.state.streams: any)}
        ItemSeparatorComponent={ListSeparator}
        keyExtractor={this.keyExtractor}
        style={styles.listContainer}
        refreshing={this.state.refreshing}
        onRefresh={this.refresh}
        renderItem={this.renderItem}
      />
    )
  }
}
<commit_msg>remove noStreams from Streams<commit_after>// @flow
/**
 * All About Olaf
 * Streaming Media page
 */

import React from 'react'
import {StyleSheet, SectionList} from 'react-native'

import * as c from '../../components/colors'
import {ListSeparator, ListSectionHeader} from '../../components/list'
import LoadingView from '../../components/loading'
import {NoticeView} from '../../components/notice'
import {TabBarIcon} from '../../components/tabbar-icon'
import {StreamRow} from './row'
import toPairs from 'lodash/toPairs'
import groupBy from 'lodash/groupBy'
import moment from 'moment-timezone'
import qs from 'querystring'
import {toLaxTitleCase as titleCase} from 'titlecase'
import type {StreamType} from './types'
import delay from 'delay'

const CENTRAL_TZ = 'America/Winnipeg'
const url = 'https://www.stolaf.edu/multimedia/api/collection'

const styles = StyleSheet.create({
  listContainer: {
    backgroundColor: c.white,
  },
})

export class StreamListView extends React.PureComponent {
  static navigationOptions = {
    tabBarLabel: 'Streaming',
    tabBarIcon: TabBarIcon('recording'),
  }

  state: {
    error: ?Error,
    loaded: boolean,
    refreshing: boolean,
    streams: Array<{title: string, data: Array<StreamType>}>,
  } = {
    error: null,
    loaded: false,
    refreshing: false,
    streams: [],
  }

  componentWillMount() {
    this.getStreams()
  }

  refresh = async () => {
    let start = Date.now()
    this.setState(() => ({refreshing: true}))

    await this.getStreams()

    // wait 0.5 seconds – if we let it go at normal speed, it feels broken.
    const elapsed = Date.now() - start
    if (elapsed < 500) {
      await delay(500 - elapsed)
    }

    this.setState(() => ({refreshing: false}))
  }

  getStreams = async (date: moment = moment.tz(CENTRAL_TZ)) => {
    try {
      const dateFrom = date.format('YYYY-MM-DD')
      const dateTo = date.add(1, 'month').format('YYYY-MM-DD')

      let params = {
        class: 'upcoming',
        sort: 'ascending',
        // eslint-disable-next-line camelcase
        date_from: dateFrom,
        // eslint-disable-next-line camelcase
        date_to: dateTo,
      }

      const streamsAPI = `${url}?${qs.stringify(params)}`
      const data = await fetchJson(streamsAPI)
      const streams = data.results

      // force title-case on the stream types, to prevent not-actually-duplicate headings
      const processed = streams
        .filter(stream => stream.category !== 'athletics')
        .map(stream => {
          const date = moment(stream.starttime, 'YYYY-MM-DD HH:mm')
          return {
            ...stream,
            category: titleCase(stream.category),
            date: date,
            $groupBy: date.format('dddd, MMMM Do'),
          }
        })

      const grouped = groupBy(processed, j => j.$groupBy)
      const mapped = toPairs(grouped).map(([title, data]) => ({title, data}))

      this.setState(() => ({
        error: null,
        loaded: true,
        streams: mapped,
      }))
    } catch (error) {
      this.setState(() => ({error: error.message}))
      console.warn(error)
    }
  }

  keyExtractor = (item: StreamType) => item.eid

  renderSectionHeader = ({section: {title}}: any) => (
    <ListSectionHeader title={title} />
  )

  renderItem = ({item}: {item: StreamType}) => <StreamRow stream={item} />

  render() {
    if (!this.state.loaded) {
      return <LoadingView />
    }

    if (this.state.error) {
      return <NoticeView text={'Error: ' + this.state.error.message} />
    }

    return (
      <SectionList
        ListEmptyComponent={<NoticeView text="No Streams" />}
        renderSectionHeader={this.renderSectionHeader}
        sections={(this.state.streams: any)}
        ItemSeparatorComponent={ListSeparator}
        keyExtractor={this.keyExtractor}
        style={styles.listContainer}
        refreshing={this.state.refreshing}
        onRefresh={this.refresh}
        renderItem={this.renderItem}
      />
    )
  }
}
<|endoftext|>


<commit_before>import {Time} from './suppClasses'

export class Schedule{
    constructor(solution) {
    	this.schedule = this.processWorkdays(solution);
    	this.events  = this.getEvents(solution);
    	this.prefContinuous = NaN;
    	this.prefFreeAfternoons = NaN;
    	this.prefFreeMornings = NaN;
    	this.prefLongLunch = NaN;
    	this.prefFreeDays = NaN;
    	this.prefFridayMorning = false;

        this.parseSchedule()
    }

    parseSchedule(){
        let prefCont = 0
        let prefFreeAfternoons = 0
        let prefFreeMornings = 0
        let prefLongLunch = 0
        let prefFreeDays = 0

        for ( var w of this.schedule.keys()){
        	if (w === 'Saturday' || w === 'Sunday')
        		continue;

            let workday = this.schedule.get(w)
            prefCont += prefContinuous(workday)
            prefFreeAfternoons += prefFreeAfternoon(workday)
            prefFreeMornings += prefFreeMorning(workday)
            prefLongLunch += prefLongLunchtimes(workday)
            prefFreeDays += prefFreeDay()

        }
        this.prefContinuous = prefCont/5
        this.prefFreeAfternoons = prefFreeAfternoons/5
        this.prefFreeMornings = prefFreeMornings/5
        this.prefLongLunch = prefLongLunch/5
        this.prefFreeDays = prefFreeDays/5
        this.prefFridayMorning = prefFreeMorning(this.schedule.get('Friday'))

    }
    processWorkdays(solution){

    	const temp_workdays = new Map();
    	const week_days = ['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
    	for (var week_day of week_days)
    		temp_workdays.set(week_day, []);
    	for (var event of solution.keys())
    		for (var instance of solution.get(event)[0].instances)
    			temp_workdays.get(week_days[instance.start.day]).push(instance);

    	const workdays = new Map();
    	for (var day of temp_workdays.keys())
    		workdays.set(day, new Workday(temp_workdays.get(day)));
    	return workdays;
    }

    getEvents(solution){
    	const eventOptions = Array.from(solution.values()).map(eventOption => eventOption[0])
    	const events = []

    	for (const event of eventOptions){
    		for (const instance of event.instances){
				events.push({
		    		title: event.event.name,
		    		start: new Date(2018, 9, 1 + instance.start.day, instance.start.time.hour, instance.start.time.min, 0, 0),
		    		end: new Date(2018, 9, 1 + instance.end.day, instance.end.time.hour, instance.end.time.min, 0, 0)
		    	})
			}
    	}
    	return events;
    }

    scheduleEvaluation(weightContinuous, weightFreeAfternoons, weightFreeMornings, weightLongLunch, weightFreeDays, weightFridayMorning){
    	return weightContinuous * this.prefContinuous + weightFreeAfternoons * this.prefFreeAfternoons + weightFreeMornings * this.prefFreeMornings
    	       + weightLongLunch * this.prefLongLunch + weightFreeAfternoons * this.prefFreeDays + weightFridayMorning * this.prefFridayMorning;

    }
}

// todo: how to handle with events that starts in one day but ends in another
export class Workday{
	constructor(events){
		this.events 			= events;
		this.workload 	= 0;
		this.begin_morning		= NaN;
		this.end_morning		= NaN;
		this.begin_afternoon 	= NaN;
		this.end_afternoon		= NaN;
        this.free_day = true;
        this.lunch_time = false;
        this.parseWorkDay();
	}

    parseWorkDay(){
      let noon = new Time(12,0)

      let bM = new Time(12,0)
      let eM = new Time(0,0)
      let bA = new Time(24,0)
      let eA = new Time(12,0)
      for ( var event of this.events){
        this.free_day = false
        this.workload += Time.interval(event.start.time, event.end.time);
        if ( Time.compare(event.start.time,noon) < 0){
          // Morning
          if ( Time.compare(event.start.time, bM) < 0)
            bM = event.start.time
          if ( Time.compare(event.end.time, eM) > 0)
            eM = event.end.time
        }else{
          // Afternoon
          if ( Time.compare(event.start.time, bA) < 0)
            bA = event.start.time
          if ( Time.compare(event.end.time, eA) > 0)
            eA = event.end.time
        }
      }

      if ( Time.interval(eM,bA) > 0)
        this.lunch_time = true

      if ( Time.compare( bM, new Time(12,0)) != 0 )
        this.begin_morning = bM;
      if ( Time.compare( eM ,new Time(0,0)) != 0)
        this.end_morning = eM;
      if ( Time.compare( bA ,new Time(24,0)) != 0)
        this.begin_afternoon = bA;
      if ( Time.compare( eA ,new Time(12,0)) != 0)
        this.end_afternoon = eA;
    }

}

function prefFreeAfternoon(workday){
	if (workday.end_afternoon instanceof Time)
      return Time.interval(workday.end_afternoon, new Time(20,0)) / (8*60); // eight hours for the afternoon := 20-12
  	return 1;
}

function prefFreeDay(workday){
	if(workday.free_day)
		return 1;
	return 1 - (workday.workload / (24*60));
}

function prefFreeMorning(workday){
	if(workday.begin_morning instanceof Time)
		return Time.interval(new Time(7, 0), workday.begin_morning) / (5*60); // five hours for the morning := 12-7 ")
	return 1;
}

function prefContinuous(workday){
    let diff = 0;
    if ( !(workday.begin_morning instanceof Time) && !(workday.begin_afternoon instanceof Time)){
      return 0
    }

    if (!(workday.begin_morning instanceof Time)){
      diff = Time.interval(workday.begin_afternoon, workday.end_afternoon)
      return (workday.workload/diff)
    }

    if (!(workday.end_afternoon instanceof Time)){
      diff = Time.interval(workday.begin_morning, workday.end_morning)
      return (workday.workload/diff)
    }

    diff = Time.interval(workday.begin_morning, workday.end_afternoon)
    return (workday.workload/diff)
}

function prefLongLunchtimes(workday){
    let r = 0.5
    if (workday.lunch_time && Time.interval(workday.end_morning, workday.begin_afternoon) > 60)
      r = 1
    return r
}

<commit_msg>Fix preferences - Free Afternoons - tested - Free Mornings   - tested - Free Days       - done (not properly test but Im confident) - Fridays Mornings- tested but not totally sure - Lunch Breaks    - tested - contiguous      - not returning exact values<commit_after>import {Time} from './suppClasses'

export class Schedule{
    constructor(solution) {
    	this.schedule = this.processWorkdays(solution);
    	this.events  = this.getEvents(solution);
    	this.prefContinuous = NaN;
    	this.prefFreeAfternoons = NaN;
    	this.prefFreeMornings = NaN;
    	this.prefLongLunch = NaN;
    	this.prefFreeDays = NaN;
    	this.prefFridayMorning = false;

        this.parseSchedule()
    }

    parseSchedule(){
        let prefCont = 0
        let prefFreeAfternoons = 0
        let prefFreeMornings = 0
        let prefLongLunch = 0
        let prefFreeDays = 0

        for ( var w of this.schedule.keys()){
        	if (w === 'Saturday' || w === 'Sunday')
        		continue;

            let workday = this.schedule.get(w)
            prefCont += prefContinuous(workday)
            prefFreeAfternoons += prefFreeAfternoon(workday)
            prefFreeMornings += prefFreeMorning(workday)
            prefLongLunch += prefLongLunchtimes(workday)
            if (this.schedule.get(w).free_day)
            	prefFreeDays += 1

        }
        this.prefContinuous = prefCont/5
        this.prefFreeAfternoons = prefFreeAfternoons/5
        this.prefFreeMornings = prefFreeMornings/5
        this.prefLongLunch = prefLongLunch/5
        this.prefFreeDays = prefFreeDays/5
        this.prefFridayMorning = prefFreeMorning(this.schedule.get('Friday'))

    }
    processWorkdays(solution){

    	const temp_workdays = new Map();
    	const week_days = ['Saturday', 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];
    	for (var week_day of week_days)
    		temp_workdays.set(week_day, []);
    	for (var event of solution.keys())
    		for (var instance of solution.get(event)[0].instances)
    			temp_workdays.get(week_days[instance.start.day]).push(instance);

    	const workdays = new Map();
    	for (var day of temp_workdays.keys())
    		workdays.set(day, new Workday(temp_workdays.get(day)));
    	return workdays;
    }

    getEvents(solution){
    	const eventOptions = Array.from(solution.values()).map(eventOption => eventOption[0])
    	const events = []

    	for (const event of eventOptions){
    		for (const instance of event.instances){
				events.push({
		    		title: event.event.name,
		    		start: new Date(2018, 9, 1 + instance.start.day, instance.start.time.hour, instance.start.time.min, 0, 0),
		    		end: new Date(2018, 9, 1 + instance.end.day, instance.end.time.hour, instance.end.time.min, 0, 0)
		    	})
			}
    	}
    	return events;
    }

    scheduleEvaluation(weightContinuous, weightFreeAfternoons, weightFreeMornings, weightLongLunch, weightFreeDays, weightFridayMorning){
    	return weightContinuous * this.prefContinuous + weightFreeAfternoons * this.prefFreeAfternoons + weightFreeMornings * this.prefFreeMornings
    	       + weightLongLunch * this.prefLongLunch + weightFreeAfternoons * this.prefFreeDays + weightFridayMorning * this.prefFridayMorning;

    }
}

// todo: how to handle with events that starts in one day but ends in another
export class Workday{
	constructor(events){
		this.events 			= events;
		this.workload 	= 0;
		this.begin_morning		= NaN;
		this.end_morning		= NaN;
		this.begin_afternoon 	= NaN;
		this.end_afternoon		= NaN;
        this.free_day = true;
        this.lunch_time = false;
        this.parseWorkDay();
	}

    parseWorkDay(){
      let noon = new Time(12,0)

      let bM = new Time(12,0)
      let eM = new Time(0,0)
      let bA = new Time(24,0)
      let eA = new Time(12,0)
      for ( var event of this.events){
        this.free_day = false
        this.workload += Time.interval(event.start.time, event.end.time);
        if ( Time.compare(event.start.time,noon) < 0){
          // Morning
          if ( Time.compare(event.start.time, bM) < 0)
            bM = event.start.time
          if ( Time.compare(event.end.time, eM) > 0)
            eM = event.end.time
        }else{
          // Afternoon
          if ( Time.compare(event.start.time, bA) < 0)
            bA = event.start.time
          if ( Time.compare(event.end.time, eA) > 0)
            eA = event.end.time
        }
      }

      if ( Time.interval(eM,bA) > 0)
        this.lunch_time = true

      if ( Time.compare( bM, new Time(12,0)) != 0 )
        this.begin_morning = bM;
      if ( Time.compare( eM ,new Time(0,0)) != 0)
        this.end_morning = eM;
      if ( Time.compare( bA ,new Time(24,0)) != 0)
        this.begin_afternoon = bA;
      if ( Time.compare( eA ,new Time(12,0)) != 0)
        this.end_afternoon = eA;
    }

}

function prefFreeAfternoon(workday){
	if (workday.end_afternoon instanceof Time)
      return Time.interval(workday.end_afternoon, new Time(20,0)) / (8*60); // eight hours for the afternoon := 20-12
  	return 1;
}

function prefFreeMorning(workday){
	if(workday.begin_morning instanceof Time)
		return Time.interval(new Time(7, 0), workday.begin_morning) / (5*60); // five hours for the morning := 12-7 ")
	return 1;
}

function prefContinuous(workday){
    let diff = 0;
    if ( !(workday.begin_morning instanceof Time) && !(workday.begin_afternoon instanceof Time)){
      return 0
    }

    if (!(workday.begin_morning instanceof Time)){
      diff = Time.interval(workday.begin_afternoon, workday.end_afternoon)
      return (workday.workload/diff)
    }

    if (!(workday.end_afternoon instanceof Time)){
      diff = Time.interval(workday.begin_morning, workday.end_morning)
      return (workday.workload/diff)
    }

    diff = Time.interval(workday.begin_morning, workday.end_afternoon)
    return (workday.workload/diff)
}

function prefLongLunchtimes(workday){
	let r = 0.5
    if (workday.lunch_time)
    	if  (Time.interval(workday.end_morning, workday.begin_afternoon) > 60)
 			r = 1
 		else
 			r = 0
  	if (!(workday.begin_morning instanceof Time) || !(workday.begin_afternoon))
  		r = 1
    return r
}

<|endoftext|>


<commit_before>/*global PKG_VERSION*/
'use strict';

import {ScrollView} from './scrollview';

export class ScrollContainer {
    constructor(aElement, aOptions = {}) {
        this._container = aElement;
        this._scrollView = new ScrollContainer.ScrollView(this, aOptions);
        this._options = aOptions;

        this._eventListener = {
            wheel: (aEvent) => {
                this.setScrollTop(this._container.scrollTop + aEvent.deltaY);
                this.setScrollLeft(this._container.scrollLeft + aEvent.deltaX);
            },
            touchstart: (aEvent) => {
                if (aEvent.defaultPrevented || aOptions.disableTouchScrollingOnContainer) {
                    return;
                }

                aEvent.preventDefault();
                let touchToTrack = aEvent.which || 0;
                let tmpMoverX = aEvent.touches[touchToTrack].pageX;
                let tmpMoverY = aEvent.touches[touchToTrack].pageY;

                let tmpMovePointer = (e) => {
                    if (e.which !== touchToTrack) {
                        return;
                    }

                    e.preventDefault();
                    let distanceX = tmpMoverX - e.touches[touchToTrack].pageX;
                    let distanceY = tmpMoverY - e.touches[touchToTrack].pageY;

                    tmpMoverX = e.touches[touchToTrack].pageX;
                    tmpMoverY = e.touches[touchToTrack].pageY;

                    this.setScrollTop(distanceY, 'add');
                    this.setScrollLeft(distanceX, 'add');
                }

                let tmpEndPointer = (e) => {
                    if (e.which !== touchToTrack) {
                        return;
                    }

                    e.preventDefault();
                    document.body.removeEventListener('touchmove', tmpMovePointer);
                    document.body.removeEventListener('touchend', tmpEndPointer);
                    document.body.removeEventListener('touchleave', tmpEndPointer);

                    tmpMovePointer = null;
                    tmpEndPointer = null;
                }

                document.body.addEventListener('touchmove', tmpMovePointer);
                document.body.addEventListener('touchend', tmpEndPointer);
                document.body.addEventListener('touchleave', tmpEndPointer);
            }
        };

        let containerHeight = this._container.clientHeight;
        let containerWidth = this._container.clientWidth;
        let scrollHeight = this._container.scrollHeight;
        let scrollWidth = this._container.scrollWidth;

        this._intervalPointer = window.setInterval(() => {
            let potentialRootElement = this._container.parentElement;

            while (potentialRootElement != undefined && potentialRootElement !== document.body) {
                potentialRootElement = potentialRootElement.parentElement;
            }

            if (potentialRootElement == undefined) {
                this.destroy();
            }
            else if (
                containerHeight !== this._container.clientHeight ||
                containerWidth !== this._container.clientWidth ||
                scrollHeight !== this._container.scrollHeight ||
                scrollWidth !== this._container.scrollWidth
                ) {
                containerHeight = this._container.clientHeight;
                containerWidth = this._container.clientWidth;
                scrollHeight = this._container.scrollHeight;
                scrollWidth = this._container.scrollWidth;

                this._scrollView.parentUpdated();
            }
        }, aOptions.checkInterval || 300);

        this._container.style.overflow = 'hidden';
        this._container.style.position = 'relative';

        Object.keys(this._eventListener).forEach((aKey) => {
            this._container.addEventListener(aKey, this._eventListener[aKey]);
        });

        this._scrollView.parentUpdated();
    }

    scrollTop(aScrollTop) {
        if (arguments.length === 0) {
            return this._container.scrollTop;
        }

        if (this._options.disableYScrolling) {
            return;
        }

        if (aScrollTop < 0) {
            aScrollTop = 0;
        }
        else if (aScrollTop > this._container.scrollHeight - this._container.clientHeight) {
            aScrollTop = this._container.scrollHeight - this._container.clientHeight;
        }

        this._scrollView.scrollTopUpdated(aScrollTop);
        this._container.scrollTop = aScrollTop;
    }

    scrollLeft(aScrollLeft) {
        if (arguments.length === 0) {
            return this._container.scrollLeft;
        }

        if (this._options.disableXScrolling) {
            return;
        }

        if (aScrollLeft < 0) {
            aScrollLeft = 0;
        }
        else if (aScrollLeft > this._container.scrollWidth - this._container.clientWidth) {
            aScrollLeft = this._container.scrollWidth - this._container.clientWidth;
        }

        this._scrollView.scrollLeftUpdated(aScrollLeft);
        this._container.scrollLeft = aScrollLeft;
    }

    destroy() {
        window.clearInterval(this._intervalPointer);

        Object.keys(this._eventListener).forEach((aKey) => {
            this._container.removeEventListener(aKey, this._eventListener[aKey]);
        });

        this._scrollView.destroy();

        this._scrollView = null;
        this._container = null;
    }
}

ScrollContainer.ScrollView = ScrollView;

export const VERSION = PKG_VERSION;<commit_msg>Fixed a bug in scroller<commit_after>/*global PKG_VERSION*/
'use strict';

import {ScrollView} from './scrollview';
import * as scrollViewHelper from './scrollviewhelper';

export class ScrollContainer {
    constructor(aElement, aOptions = {}) {
        this._container = aElement;
        this._scrollView = new ScrollContainer.ScrollView(this, aOptions);
        this._options = aOptions;

        this._eventListener = {
            wheel: (aEvent) => {
                this.scrollTop(this._container.scrollTop + aEvent.deltaY);
                this.scrollLeft(this._container.scrollLeft + aEvent.deltaX);
            },
            touchstart: (aEvent) => {
                if (aEvent.defaultPrevented || aOptions.disableTouchScrollingOnContainer) {
                    return;
                }

                aEvent.preventDefault();
                let touchToTrack = aEvent.which || 0;
                let tmpMoverX = aEvent.touches[touchToTrack].pageX;
                let tmpMoverY = aEvent.touches[touchToTrack].pageY;

                let tmpMovePointer = (e) => {
                    if (e.which !== touchToTrack) {
                        return;
                    }

                    e.preventDefault();
                    let distanceX = tmpMoverX - e.touches[touchToTrack].pageX;
                    let distanceY = tmpMoverY - e.touches[touchToTrack].pageY;

                    tmpMoverX = e.touches[touchToTrack].pageX;
                    tmpMoverY = e.touches[touchToTrack].pageY;

                    this.scrollTop(this._container.scrollTop + distanceY);
                    this.scrollLeft(this._container.scrollLeft + distanceX);
                }

                let tmpEndPointer = (e) => {
                    if (e.which !== touchToTrack) {
                        return;
                    }

                    e.preventDefault();
                    document.body.removeEventListener('touchmove', tmpMovePointer);
                    document.body.removeEventListener('touchend', tmpEndPointer);
                    document.body.removeEventListener('touchleave', tmpEndPointer);

                    tmpMovePointer = null;
                    tmpEndPointer = null;
                }

                document.body.addEventListener('touchmove', tmpMovePointer);
                document.body.addEventListener('touchend', tmpEndPointer);
                document.body.addEventListener('touchleave', tmpEndPointer);
            }
        };

        let containerHeight = this._container.clientHeight;
        let containerWidth = this._container.clientWidth;
        let scrollHeight = this._container.scrollHeight;
        let scrollWidth = this._container.scrollWidth;

        this._intervalPointer = window.setInterval(() => {
            let potentialRootElement = this._container.parentElement;

            while (potentialRootElement != undefined && potentialRootElement !== document.body) {
                potentialRootElement = potentialRootElement.parentElement;
            }

            if (potentialRootElement == undefined) {
                this.destroy();
            }
            else if (
                containerHeight !== this._container.clientHeight ||
                containerWidth !== this._container.clientWidth ||
                scrollHeight !== this._container.scrollHeight ||
                scrollWidth !== this._container.scrollWidth
                ) {
                containerHeight = this._container.clientHeight;
                containerWidth = this._container.clientWidth;
                scrollHeight = this._container.scrollHeight;
                scrollWidth = this._container.scrollWidth;

                this._scrollView.parentUpdated();
            }
        }, aOptions.checkInterval || 300);

        this._container.style.overflow = 'hidden';
        this._container.style.position = 'relative';

        Object.keys(this._eventListener).forEach((aKey) => {
            this._container.addEventListener(aKey, this._eventListener[aKey]);
        });

        this._scrollView.parentUpdated();
    }

    scrollTop(aScrollTop) {
        if (arguments.length === 0) {
            return this._container.scrollTop;
        }

        if (this._options.disableYScrolling) {
            return;
        }

        if (aScrollTop < 0) {
            aScrollTop = 0;
        }
        else if (aScrollTop > this._container.scrollHeight - this._container.clientHeight) {
            aScrollTop = this._container.scrollHeight - this._container.clientHeight;
        }

        this._scrollView.scrollTopUpdated(aScrollTop);
        this._container.scrollTop = aScrollTop;
    }

    scrollLeft(aScrollLeft) {
        if (arguments.length === 0) {
            return this._container.scrollLeft;
        }

        if (this._options.disableXScrolling) {
            return;
        }

        if (aScrollLeft < 0) {
            aScrollLeft = 0;
        }
        else if (aScrollLeft > this._container.scrollWidth - this._container.clientWidth) {
            aScrollLeft = this._container.scrollWidth - this._container.clientWidth;
        }

        this._scrollView.scrollLeftUpdated(aScrollLeft);
        this._container.scrollLeft = aScrollLeft;
    }

    destroy() {
        window.clearInterval(this._intervalPointer);

        Object.keys(this._eventListener).forEach((aKey) => {
            this._container.removeEventListener(aKey, this._eventListener[aKey]);
        });

        this._scrollView.destroy();

        this._scrollView = null;
        this._container = null;
    }
}

ScrollContainer.ScrollView = ScrollView;
ScrollContainer.scrollViewHelper = scrollViewHelper;

export const VERSION = PKG_VERSION;<|endoftext|>


<commit_before>/**
 *
 * scsynth - boots a supercollider synthesis server process
 *
 *  SuperCollider comes with an executable called scsynth
 *  which can be communicated with via udp OSC
 *
 *  The primary way to send messages in with sendMsg
 *  eg. server.sendMsg('/s_new', ['defName', 440])

 *  and the responses are emitted as 'OSC'
 *  eg. server.on('OSC', function(msg) {  ...  });
 *
 * methods:
 *   boot        - boot an scsynth process
 *   quit
 *   connect     - connect via udp OSC
 *   disconnect
 *   sendMsg     - send an OSC message
 *
 * emits:
 *    'out'   - stdout text from the server
 *    'error' - stderr text from the server or OSC error messages
 *    'exit'  - when server exits
 *    'close' - when server closes the UDP connection
 *    'OSC'   - OSC responses from the server
 */

import Immutable from 'immutable';
import * as alloc from './allocators';

var
  _ = require('underscore'),
  EventEmitter = require('events').EventEmitter,
  spawn = require('child_process').spawn,
  defaultOptions = require('./default-server-options.json'),
  Logger = require('./logger'),
  dgram = require('dgram'),
  osc = require('osc-min'),
  Q = require('q');

const keys = {
  NODE_IDS: 'nodeAllocator',
  CONTROL_BUSSES: 'controlBusAllocator',
  AUDIO_BUSSES: 'audioBusAllocator',
  BUFFERS: 'bufferAllocator'
};

export class Server extends EventEmitter {

  /**
   * @param {Object} options - server command line options
   */
  constructor(options={}) {
    super();
    this.options = _.defaults(options, defaultOptions);
    this.process = null;
    this.isRunning = false;
    this.log = new Logger(this.options.debug, this.options.echo);
    this.resetState();
  }

  resetState() {
    var state = Immutable.Map();
    state = state.set(keys.NODE_IDS, this.options.initialNodeID - 1);

    var numAudioChannels = this.options.numPrivateAudioBusChannels +
      this.options.numInputBusChannels +
      this.options.numOutputBusChannels;
    var ab = alloc.initialBlockState(numAudioChannels);
    ab = alloc.reserveBlock(ab, 0, this.options.numInputBusChannels + this.options.numOutputBusChannels);
    state = state.set(keys.AUDIO_BUSSES, ab);

    var cb = alloc.initialBlockState(this.options.numControlBusChannels);
    state = state.set(keys.CONTROL_BUSSES, cb);

    var bb = alloc.initialBlockState(this.options.numBuffers);
    state = state.set(keys.BUFFERS, cb);

    this.state = state;
  }

  /**
   * Format command line args for scsynth
   *
   * not yet fully implemented
   *
   * @return {array} list of non-default args
   */
  args() {
    var o = [];
    o.push(this.options.protocol === 'udp' ? '-u' : '-t');
    o.push(this.options.serverPort);
    return o;
  }

  /**
   * boot
   *
   * start scsynth and establish a pipe connection
   * to receive stdout and stderr
   *
   * listen for system events and emit: exit out error
   */
  boot() {
    var
      self = this,
      execPath = this.options.scsynth,
      args = this.args(),
      d = Q.defer();

    this.log.dbug(execPath + ' ' + args.join(' '));
    this.process = spawn(execPath, args,
      {
        cwd: this.options.cwd
      });
    this.log.dbug('Spawned pid: ' + this.process.pid);

    this.process.on('error', (err) => {
      this.log.err('Server error ' + err);
      this.emit('exit', err);
      // this.isRunning = false;
    });
    this.process.on('close', (code, signal) => {
      this.log.dbug('Server closed ' + code);
      this.emit('exit', code);
      this.isRunning = false;
    });
    this.process.on('exit', (code, signal) => {
      this.log.dbug('Server exited ' + code);
      this.emit('exit', code);
      this.isRunning = false;
    });

    this.process.stdout.on('data', (data) => {
      this.log.stdout('' + data);
      this.emit('out', data);
      this.isRunning = true;
    });
    this.process.stderr.on('data', (data) => {
      this.log.stderr('' + data);
      this.emit('stderr', data);
      this.isRunning = true;
    });

    setTimeout(() => {
      if (this.isRunning) {
        d.resolve();
      } else {
        d.reject();
      }
    }, 100);

    return d.promise;
  }

  /**
   * quit
   *
   * kill scsynth process
   */
  quit() {
    if (this.process) {
      this.process.kill('SIGTERM');
      this.process = null;
    }
  }

  connect() {
    var self = this;
    this.udp = dgram.createSocket('udp4');

    this.udp.on('message', function(msgbuf, rinfo) {
      var msg = osc.fromBuffer(msgbuf);
      self.log.rcvosc(msg);
      self.emit('OSC', msg);
    });

    this.udp.on('error', function(e) {
      self.log.err(e);
      self.emit('error', e);
    });
    this.udp.on('listening', function() {
      self.log.dbug('udp is listening');
    });
    this.udp.on('close', function(e) {
      self.log.dbug('udp close' + e);
      self.emit('close', e);
    });
  }

  disconnect() {
    if (this.udp) {
      this.udp.close();
      delete this.udp;
    }
  }

  /**
   * @param {String} address - OSC command, referred to as address
   * @param {Array} args
   */
  sendMsg(address, args) {
    var buf = osc.toBuffer({
      address: address,
      args: args
    });
    this.log.sendosc(address + ' ' + args.join(' '));
    this.udp.send(buf, 0, buf.length, this.options.serverPort, this.options.host);
  }

  nextNodeID() {
    return this._mutateState(keys.NODE_IDS, alloc.increment);
  }

  // temporary raw allocator calls
  allocAudioBus(numChannels=1) {
    return this._allocBlock(keys.AUDIO_BUSSES, numChannels);
  }
  allocControlBus(numChannels=1) {
    return this._allocBlock(keys.CONTROL_BUSSES, numChannels);
  }
  /**
   * Allocate a buffer id.
   *
   * Note that numChannels is specified when creating the buffer.
   *
   * @param {int} numConsecutive - consecutively numbered buffers are needed by VOsc and VOsc3.
   * @returns {int}
   */
  allocBufferID(numConsecutive=1) {
    return this._allocBlock(keys.BUFFERS, numConsecutive);
  }

  // these require you to remember the channels and mess it up
  // if you free it wrong
  freeAudioBus(index, numChannels) {
    return this._freeBlock(keys.AUDIO_BUSSES, index, numChannels);
  }
  freeControlBus(index, numChannels) {
    return this._freeBlock(keys.CONTROL_BUSSES, index, numChannels);
  }
  freeBuffer(index, numChannels) {
    return this._freeBlock(keys.BUFFERS, index, numChannels);
  }

  // private
  /**
   * Fetch one part of the state,
   * mutate it with the callback,
   * save state and return the result.
   *
   * @returns {any} result
   */
  _mutateState(key, fn) {
    var result, state;
    [result, state] = fn(this.state.get(key));
    this.state = this.state.set(key, state);
    return result;
  }
  _mutateStateNoReturn(key, fn) {
    var state = fn(this.state.get(key));
    this.state = this.state.set(key, state);
  }
  _allocBlock(key, numChannels) {
    return this._mutateState(key,
      (state) => alloc.allocBlock(state, numChannels));
  }
  _freeBlock(key, index, numChannels) {
    return this._mutateStateNoReturn(key,
      (state) => alloc.freeBlock(state, index, numChannels));
  }
}

/**
 * boot a server with options
 * @returns {Promise}
 */
export function boot(options) {
  var resolveOptions = require('./resolveOptions');
  return resolveOptions(null, options).then(function(opts) {
    var s = new Server(opts);
    return s.boot().then(function() {
      s.connect();
      return s;
    });
  });
}
<commit_msg>temp: increase wait before declaring that server is up and running<commit_after>/**
 *
 * scsynth - boots a supercollider synthesis server process
 *
 *  SuperCollider comes with an executable called scsynth
 *  which can be communicated with via udp OSC
 *
 *  The primary way to send messages in with sendMsg
 *  eg. server.sendMsg('/s_new', ['defName', 440])

 *  and the responses are emitted as 'OSC'
 *  eg. server.on('OSC', function(msg) {  ...  });
 *
 * methods:
 *   boot        - boot an scsynth process
 *   quit
 *   connect     - connect via udp OSC
 *   disconnect
 *   sendMsg     - send an OSC message
 *
 * emits:
 *    'out'   - stdout text from the server
 *    'error' - stderr text from the server or OSC error messages
 *    'exit'  - when server exits
 *    'close' - when server closes the UDP connection
 *    'OSC'   - OSC responses from the server
 */

import Immutable from 'immutable';
import * as alloc from './allocators';

var
  _ = require('underscore'),
  EventEmitter = require('events').EventEmitter,
  spawn = require('child_process').spawn,
  defaultOptions = require('./default-server-options.json'),
  Logger = require('./logger'),
  dgram = require('dgram'),
  osc = require('osc-min'),
  Q = require('q');

const keys = {
  NODE_IDS: 'nodeAllocator',
  CONTROL_BUSSES: 'controlBusAllocator',
  AUDIO_BUSSES: 'audioBusAllocator',
  BUFFERS: 'bufferAllocator'
};

export class Server extends EventEmitter {

  /**
   * @param {Object} options - server command line options
   */
  constructor(options={}) {
    super();
    this.options = _.defaults(options, defaultOptions);
    this.process = null;
    this.isRunning = false;
    this.log = new Logger(this.options.debug, this.options.echo);
    this.resetState();
  }

  resetState() {
    var state = Immutable.Map();
    state = state.set(keys.NODE_IDS, this.options.initialNodeID - 1);

    var numAudioChannels = this.options.numPrivateAudioBusChannels +
      this.options.numInputBusChannels +
      this.options.numOutputBusChannels;
    var ab = alloc.initialBlockState(numAudioChannels);
    ab = alloc.reserveBlock(ab, 0, this.options.numInputBusChannels + this.options.numOutputBusChannels);
    state = state.set(keys.AUDIO_BUSSES, ab);

    var cb = alloc.initialBlockState(this.options.numControlBusChannels);
    state = state.set(keys.CONTROL_BUSSES, cb);

    var bb = alloc.initialBlockState(this.options.numBuffers);
    state = state.set(keys.BUFFERS, cb);

    this.state = state;
  }

  /**
   * Format command line args for scsynth
   *
   * not yet fully implemented
   *
   * @return {array} list of non-default args
   */
  args() {
    var o = [];
    o.push(this.options.protocol === 'udp' ? '-u' : '-t');
    o.push(this.options.serverPort);
    return o;
  }

  /**
   * boot
   *
   * start scsynth and establish a pipe connection
   * to receive stdout and stderr
   *
   * listen for system events and emit: exit out error
   */
  boot() {
    var
      self = this,
      execPath = this.options.scsynth,
      args = this.args(),
      d = Q.defer();

    this.log.dbug(execPath + ' ' + args.join(' '));
    this.process = spawn(execPath, args,
      {
        cwd: this.options.cwd
      });
    this.log.dbug('Spawned pid: ' + this.process.pid);

    this.process.on('error', (err) => {
      this.log.err('Server error ' + err);
      this.emit('exit', err);
      // this.isRunning = false;
    });
    this.process.on('close', (code, signal) => {
      this.log.dbug('Server closed ' + code);
      this.emit('exit', code);
      this.isRunning = false;
    });
    this.process.on('exit', (code, signal) => {
      this.log.dbug('Server exited ' + code);
      this.emit('exit', code);
      this.isRunning = false;
    });

    this.process.stdout.on('data', (data) => {
      this.log.stdout('' + data);
      this.emit('out', data);
      // should say the magical words:
      // "SuperCollider 3 server ready"
      // and resolve if not already
      this.isRunning = true;
    });
    this.process.stderr.on('data', (data) => {
      this.log.stderr('' + data);
      this.emit('stderr', data);
      this.isRunning = true;
    });

    setTimeout(() => {
      if (this.isRunning) {
        d.resolve();
      } else {
        d.reject();
      }
    }, 300);

    return d.promise;
  }

  /**
   * quit
   *
   * kill scsynth process
   */
  quit() {
    if (this.process) {
      this.process.kill('SIGTERM');
      this.process = null;
    }
  }

  connect() {
    var self = this;
    this.udp = dgram.createSocket('udp4');

    this.udp.on('message', function(msgbuf, rinfo) {
      var msg = osc.fromBuffer(msgbuf);
      self.log.rcvosc(msg);
      self.emit('OSC', msg);
    });

    this.udp.on('error', function(e) {
      self.log.err(e);
      self.emit('error', e);
    });
    this.udp.on('listening', function() {
      self.log.dbug('udp is listening');
    });
    this.udp.on('close', function(e) {
      self.log.dbug('udp close' + e);
      self.emit('close', e);
    });
  }

  disconnect() {
    if (this.udp) {
      this.udp.close();
      delete this.udp;
    }
  }

  /**
   * @param {String} address - OSC command, referred to as address
   * @param {Array} args
   */
  sendMsg(address, args) {
    var buf = osc.toBuffer({
      address: address,
      args: args
    });
    this.log.sendosc(address + ' ' + args.join(' '));
    this.udp.send(buf, 0, buf.length, this.options.serverPort, this.options.host);
  }

  nextNodeID() {
    return this._mutateState(keys.NODE_IDS, alloc.increment);
  }

  // temporary raw allocator calls
  allocAudioBus(numChannels=1) {
    return this._allocBlock(keys.AUDIO_BUSSES, numChannels);
  }
  allocControlBus(numChannels=1) {
    return this._allocBlock(keys.CONTROL_BUSSES, numChannels);
  }
  /**
   * Allocate a buffer id.
   *
   * Note that numChannels is specified when creating the buffer.
   *
   * @param {int} numConsecutive - consecutively numbered buffers are needed by VOsc and VOsc3.
   * @returns {int}
   */
  allocBufferID(numConsecutive=1) {
    return this._allocBlock(keys.BUFFERS, numConsecutive);
  }

  // these require you to remember the channels and mess it up
  // if you free it wrong
  freeAudioBus(index, numChannels) {
    return this._freeBlock(keys.AUDIO_BUSSES, index, numChannels);
  }
  freeControlBus(index, numChannels) {
    return this._freeBlock(keys.CONTROL_BUSSES, index, numChannels);
  }
  freeBuffer(index, numChannels) {
    return this._freeBlock(keys.BUFFERS, index, numChannels);
  }

  // private
  /**
   * Fetch one part of the state,
   * mutate it with the callback,
   * save state and return the result.
   *
   * @returns {any} result
   */
  _mutateState(key, fn) {
    var result, state;
    [result, state] = fn(this.state.get(key));
    this.state = this.state.set(key, state);
    return result;
  }
  _mutateStateNoReturn(key, fn) {
    var state = fn(this.state.get(key));
    this.state = this.state.set(key, state);
  }
  _allocBlock(key, numChannels) {
    return this._mutateState(key,
      (state) => alloc.allocBlock(state, numChannels));
  }
  _freeBlock(key, index, numChannels) {
    return this._mutateStateNoReturn(key,
      (state) => alloc.freeBlock(state, index, numChannels));
  }
}

/**
 * boot a server with options
 * @returns {Promise}
 */
export function boot(options) {
  var resolveOptions = require('./resolveOptions');
  return resolveOptions(null, options).then(function(opts) {
    var s = new Server(opts);
    return s.boot().then(function() {
      s.connect();
      return s;
    });
  });
}
<|endoftext|>


<commit_before>//selection logic separated into another class for clarity, but included in this file for simplicity
var SelectionManager = L.Class.extend({

	initialize: function (map) {
		this._map = map;
	},

	getSelectedMarkers: function() {
		var markers = Object.keys(this._allSelectedMarkers).sort().map(function(markerId) { //sort is just so the order is predictable, makes debugging easier
			return this._allSelectedMarkers[markerId];
		}.bind(this));
		return markers;
	},

	enable: function() {
		this._clearExistingSelection();
	},

	_clearExistingSelection: function() {
		this._allSelectedMarkers = {};
		this._previouslySelectedMarkers = [];
		this._currentlySelectedMarkers = [];
	},

	disable: function() {
		for (key in this._allSelectedMarkers) {
			var marker = this._allSelectedMarkers[key];
			this._toggleSelection(marker, false);
		}
		this._clearExistingSelection();
	},

	start: function() {
		this._currentlySelectedMarkers = [];
		this._findAllLoadedMarkers();
	},

	_findAllLoadedMarkers: function() {
		this._allLoadedMarkers = {};
		var fakeBounds = {contains: function() {return true;}};//TODO refactor enumerate to remove this
		this._enumerateMarkers(fakeBounds, this._map._layers, function(marker) {
			var key = L.Util.stamp(marker);
			this._allLoadedMarkers[key] = marker;
		}.bind(this));
	},

	update: function(latLngBounds) {
		var newlySelectedMarkers = {};
		this._enumerateMarkers(latLngBounds, this._map._layers, function(marker) {
			var key = L.Util.stamp(marker);
			newlySelectedMarkers[key] = marker;
		}.bind(this));
		var mismatches = this._outerJoinArrays(Object.keys(this._currentlySelectedMarkers), Object.keys(newlySelectedMarkers));
		var idToSelect = mismatches.in2ButNot1;
		var idToDeselect = mismatches.in1ButNot2;
		for (var i = 0; i < idToSelect.length; i++) {
			var marker = this._allLoadedMarkers[idToSelect[i]];
			this._toggleSelection(marker, true);
		}
		for (var i = 0; i < idToDeselect.length; i++) {
			//if this marker was selected in a previous run, don't de-select it now
			if (this._previouslySelectedMarkers.indexOf(idToDeselect[i]) == -1) {
				var marker = this._allLoadedMarkers[idToDeselect[i]];
				this._toggleSelection(marker, false);
			}
		}
		this._currentlySelectedMarkers = newlySelectedMarkers;
	},

	finish: function(bounds) {
		this._enumerateMarkers(bounds, this._map._layers, function(marker) {
			if (this._allSelectedMarkers == null) {
				this._allSelectedMarkers = {};
			}
			var key = L.Util.stamp(marker);
			this._allSelectedMarkers[key] = marker; //may already be there, but just override it
		}.bind(this));
		this._previouslySelectedMarkers = this._previouslySelectedMarkers.concat(Object.keys(this._currentlySelectedMarkers));
	},

	_enumerateMarkers: function(bounds, layers, callback) {
		Object.keys(layers).forEach(function (key) {
			var layer = layers[key];
			if (layer instanceof L.Marker) {
				if (bounds.contains(layer.getLatLng())) {
					callback(layer);
				}
			} else if (layer._layers != undefined) {
				this._enumerateMarkers(bounds, layer._layers, callback);
			}
		}, this);
	},

	_toggleSelection(marker, select) {
		if (select) {
			L.DomUtil.addClass(marker._icon, 'marker-highlight');
		} else {
			L.DomUtil.removeClass(marker._icon, 'marker-highlight');
		}
	},

	//both arrays must contain only numbers
	_outerJoinArrays: function (arr1, arr2) {
		function numericalSort(a, b) {
			return a - b;
		}
		arr1 = arr1.sort(numericalSort);
		arr2 = arr2.sort(numericalSort);
		var i = 0;
		var j = 0;
		var in1ButNot2 = [];
		var in2ButNot1 = [];
		while (i < arr1.length || j < arr2.length) {
			if (i == arr1.length) {
				//run out of array 1, so everything left in array 2 must be missing from 1
				in2ButNot1.push(arr2[j]); //TODO could just merge the arrays to be more efficient here
				j++;
			} else if (j == arr2.length) {
				//run out of array 2, so everything left in array 1 must be missing from 2
				in1ButNot2.push(arr1[i]); //TODO could just merge the arrays to be more efficient here
				i++;
			} else if (arr1[i] == arr2[j]) {
				//in both, move on
				i++;
				j++;
			} else if (arr1[i] < arr2[j]) {
				//must be missing from arr2
				in1ButNot2.push(arr1[i]);
				i++;
			} else {//if (arr1[i] > arr2[j]) {
				//must be missing from arr1
				in2ButNot1.push(arr2[j]);
				j++;
			}
		}
		if (i != arr1.length || j != arr2.length) {
			console.error("not all the things were consumed");
			console.error("arr1.length=" + arr1.length + ", i=" + i);
			console.error("arr2.length=" + arr2.length + ", j=" + j);
		}
		return {
			in1ButNot2: in1ButNot2,
			in2ButNot1: in2ButNot1
		};
	}
});

L.Control.BoxSelector = L.Control.extend({

	options: {
		highlightOnDrag: true //on by default but could cause poor performance if there is a large number of markers
	},

	initialize: function (options) {
		L.setOptions(this, options);
		if (this.options.actions == null) {
			//set default action
			this.options.actions = {
				alert: {
					display: "Display selected coords",
					action:	L.Control.BoxSelector.Actions.Alert
				}
			}
		}
	},

	onAdd: function (map) {
		this._map = map;
		this._mapcontainer = map._container;
		this._pane = map._panes.overlayPane;
		this._addHooks();
		this._manager = new SelectionManager(this._map);

		//set up select icon
		this._selectorContainer = L.DomUtil.create('div', 'leaflet-control-box-selector leaflet-bar leaflet-control boxselector-control boxselector-hidden');
		this._buttonBar = L.DomUtil.create('div', 'leaflet-bar-part leaflet-bar-part-single boxselector-button-bar', this._selectorContainer);
		this._toggleElement = L.DomUtil.create('a', 'boxselector-icon', this._buttonBar);
		this._toggleElement.id = 'boxselector-icon';
		this._toggleElement.href = '#';
		var iconWrapper = L.DomUtil.create('div', 'icon-wrapper', this._toggleElement);
		var icon = L.DomUtil.create('img', 'icon', iconWrapper);
		icon.src = '../src/select-icon.png';

		//set up dropdown icon
		var dropdownButton = L.DomUtil.create('a', 'boxselector-dropdown-button', this._buttonBar);
		dropdownButton.id = 'boxselector-dropdown-button';
		dropdownButton.href = '#';
		var dropdownIconWrapper = L.DomUtil.create('div', 'boxselector-dropdown-icon-wrapper', dropdownButton);
		var dropdownIcon = L.DomUtil.create('div', 'boxselector-dropdown-icon', dropdownIconWrapper);
		L.DomEvent.on(dropdownButton, 'click', this._expandCollapse, this)

		//set up drop down (hidden to start with)
		this._expanded = false;
		this._dropdown = L.DomUtil.create('div', 'boxselector-dropdown', this._selectorContainer);
		for (var actionId in this.options.actions) {
			var dropdownEntry = L.DomUtil.create('a', 'boxselector-dropdown-entry', this._dropdown);
			dropdownEntry.actionId = actionId;
			dropdownEntry.href = '#';
			var label = L.DomUtil.create('span', '', dropdownEntry);
			label.innerHTML = this.options.actions[actionId].display;
			label.actionId = actionId;

			L.DomEvent.on(dropdownEntry, 'click', this._actionItemClick, this);
		}

		L.DomEvent.on(this._toggleElement, 'click', this._toggle, this);

		//ensure we tear down
		this._map.on('unload', this._removeHooks, this);

		return this._selectorContainer;
	},

	_expandCollapse: function() {
		if (this._expanded) {
			L.DomUtil.addClass(this._selectorContainer, 'boxselector-hidden');
			L.DomUtil.removeClass(this._selectorContainer, 'boxselector-expanded');
			this._expanded = false;
		} else {
			L.DomUtil.addClass(this._selectorContainer, 'boxselector-expanded');
			L.DomUtil.removeClass(this._selectorContainer, 'boxselector-hidden');
			this._expanded = true;
		}
	},

	_actionItemClick: function(e) {
		var actionItemElement = e.target;
		var actionId = actionItemElement.actionId;
		var action = this.options.actions[actionId].action;
		this._expandCollapse();
		action(this.getSelectedMarkers());
	},

	_addHooks: function () {
		L.DomEvent.on(this._mapcontainer, 'mousedown', this._onMouseDown, this);
	},

	_removeHooks: function () {
		L.DomEvent.off(this._mapcontainer, 'mousedown', this._onMouseDown, this);
	},

	_toggle: function() {
		this._setEnabled(!this._isEnabled());
	},

	_isEnabled: function() {
		return this._selectionEnabled;
	},

	_setEnabled: function(enabled) {
		this._selectionEnabled = enabled;
		if (enabled) {
			//enable selection
			this._map.dragging.disable();
			L.DomUtil.addClass(this._buttonBar, 'selection-enabled');
			this._manager.enable();
		} else {
			//disable selection
			this._map.dragging.enable();
			L.DomUtil.removeClass(this._buttonBar, 'selection-enabled');
			this._manager.disable();
		}
	},

	moved: function () {
		return this._moved;
	},

	_resetState: function () {
		this._moved = false;
	},

	_onMouseDown: function (e) {
		if (!this._isEnabled() || (e.button !== 0)) {
			return false;
		}

		this._resetState();

		L.DomUtil.disableTextSelection();
		L.DomUtil.disableImageDrag();

		this._startPoint = this._map.mouseEventToContainerPoint(e);

		L.DomEvent.on(document, {
			contextmenu: L.DomEvent.stop,
			mousemove: this._onMouseMove,
			mouseup: this._onMouseUp,
			keydown: this._onKeyDown
		}, this);

		this._manager.start();
	},

	_onMouseMove: function (e) {
		if (!this._moved) {
			this._moved = true;

			this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._mapcontainer);
			L.DomUtil.addClass(this._mapcontainer, 'leaflet-crosshair');
		}

		this._point = this._map.mouseEventToContainerPoint(e);

		var bounds = new L.Bounds(this._point, this._startPoint),
		    size = bounds.getSize();

		L.DomUtil.setPosition(this._box, bounds.min);

		this._box.style.width  = size.x + 'px';
		this._box.style.height = size.y + 'px';

		if (this.options.highlightOnDrag) {
			var latLngBounds = new L.LatLngBounds(
				this._map.containerPointToLatLng(this._startPoint),
				this._map.containerPointToLatLng(this._point));

			this._manager.update(latLngBounds);
		}
	},

	_finish: function () {
		if (this._moved) {
			L.DomUtil.remove(this._box);
			L.DomUtil.removeClass(this._mapcontainer, 'leaflet-crosshair');
		}

		L.DomUtil.enableTextSelection();
		L.DomUtil.enableImageDrag();

		L.DomEvent.off(document, {
			contextmenu: L.DomEvent.stop,
			mousemove: this._onMouseMove,
			mouseup: this._onMouseUp,
			keydown: this._onKeyDown
		}, this);
	},

	_onMouseUp: function (e) {
		if ((e.which !== 1) && (e.button !== 1)) { return; }

		this._finish();

		if (!this._moved) { return; }
		// Postpone to next JS tick so internal click event handling
		// still see it as "moved".
		setTimeout(L.bind(this._resetState, this), 0);

		var bounds = new L.LatLngBounds(
			this._map.containerPointToLatLng(this._startPoint),
			this._map.containerPointToLatLng(this._point));

		if (!this.options.highlightOnDrag) {
			//if we aren't highlighting on drag, fool the manager into updating
			this._manager.update(bounds);
		}
		this._manager.finish(bounds);
	},

	getSelectedMarkers: function() {
		return this._manager.getSelectedMarkers();
	},

	_onKeyDown: function (e) {
		if (e.keyCode === 27) {
			this._finish();
		}
	}
});
L.Control.BoxSelector.Actions = {};
L.Control.BoxSelector.Actions.Alert = function(selectedMarkers) {
	var output = "";
	for (var i = 0; i < selectedMarkers.length; i++) {
		var marker = selectedMarkers[i];
		output += marker.name
		output += ": ";
		output += marker.getLatLng().lat;
		output += ",";
		output += marker.getLatLng().lng;
		output += "\n";
	}
	alert(output);
}
<commit_msg>Dead code<commit_after>//selection logic separated into another class for clarity, but included in this file for simplicity
var SelectionManager = L.Class.extend({

	initialize: function (map) {
		this._map = map;
	},

	getSelectedMarkers: function() {
		var markers = Object.keys(this._allSelectedMarkers).sort().map(function(markerId) { //sort is just so the order is predictable, makes debugging easier
			return this._allSelectedMarkers[markerId];
		}.bind(this));
		return markers;
	},

	enable: function() {
		this._clearExistingSelection();
	},

	_clearExistingSelection: function() {
		this._allSelectedMarkers = {};
		this._previouslySelectedMarkers = [];
		this._currentlySelectedMarkers = [];
	},

	disable: function() {
		for (key in this._allSelectedMarkers) {
			var marker = this._allSelectedMarkers[key];
			this._toggleSelection(marker, false);
		}
		this._clearExistingSelection();
	},

	start: function() {
		this._currentlySelectedMarkers = [];
		this._findAllLoadedMarkers();
	},

	_findAllLoadedMarkers: function() {
		this._allLoadedMarkers = {};
		var fakeBounds = {contains: function() {return true;}};//TODO refactor enumerate to remove this
		this._enumerateMarkers(fakeBounds, this._map._layers, function(marker) {
			var key = L.Util.stamp(marker);
			this._allLoadedMarkers[key] = marker;
		}.bind(this));
	},

	update: function(latLngBounds) {
		var newlySelectedMarkers = {};
		this._enumerateMarkers(latLngBounds, this._map._layers, function(marker) {
			var key = L.Util.stamp(marker);
			newlySelectedMarkers[key] = marker;
		}.bind(this));
		var mismatches = this._outerJoinArrays(Object.keys(this._currentlySelectedMarkers), Object.keys(newlySelectedMarkers));
		var idToSelect = mismatches.in2ButNot1;
		var idToDeselect = mismatches.in1ButNot2;
		for (var i = 0; i < idToSelect.length; i++) {
			var marker = this._allLoadedMarkers[idToSelect[i]];
			this._toggleSelection(marker, true);
		}
		for (var i = 0; i < idToDeselect.length; i++) {
			//if this marker was selected in a previous run, don't de-select it now
			if (this._previouslySelectedMarkers.indexOf(idToDeselect[i]) == -1) {
				var marker = this._allLoadedMarkers[idToDeselect[i]];
				this._toggleSelection(marker, false);
			}
		}
		this._currentlySelectedMarkers = newlySelectedMarkers;
	},

	finish: function(bounds) {
		this._enumerateMarkers(bounds, this._map._layers, function(marker) {
			if (this._allSelectedMarkers == null) {
				this._allSelectedMarkers = {};
			}
			var key = L.Util.stamp(marker);
			this._allSelectedMarkers[key] = marker; //may already be there, but just override it
		}.bind(this));
		this._previouslySelectedMarkers = this._previouslySelectedMarkers.concat(Object.keys(this._currentlySelectedMarkers));
	},

	_enumerateMarkers: function(bounds, layers, callback) {
		Object.keys(layers).forEach(function (key) {
			var layer = layers[key];
			if (layer instanceof L.Marker) {
				if (bounds.contains(layer.getLatLng())) {
					callback(layer);
				}
			} else if (layer._layers != undefined) {
				this._enumerateMarkers(bounds, layer._layers, callback);
			}
		}, this);
	},

	_toggleSelection(marker, select) {
		if (select) {
			L.DomUtil.addClass(marker._icon, 'marker-highlight');
		} else {
			L.DomUtil.removeClass(marker._icon, 'marker-highlight');
		}
	},

	//both arrays must contain only numbers
	_outerJoinArrays: function (arr1, arr2) {
		function numericalSort(a, b) {
			return a - b;
		}
		arr1 = arr1.sort(numericalSort);
		arr2 = arr2.sort(numericalSort);
		var i = 0;
		var j = 0;
		var in1ButNot2 = [];
		var in2ButNot1 = [];
		while (i < arr1.length || j < arr2.length) {
			if (i == arr1.length) {
				//run out of array 1, so everything left in array 2 must be missing from 1
				in2ButNot1.push(arr2[j]); //TODO could just merge the arrays to be more efficient here
				j++;
			} else if (j == arr2.length) {
				//run out of array 2, so everything left in array 1 must be missing from 2
				in1ButNot2.push(arr1[i]); //TODO could just merge the arrays to be more efficient here
				i++;
			} else if (arr1[i] == arr2[j]) {
				//in both, move on
				i++;
				j++;
			} else if (arr1[i] < arr2[j]) {
				//must be missing from arr2
				in1ButNot2.push(arr1[i]);
				i++;
			} else {//if (arr1[i] > arr2[j]) {
				//must be missing from arr1
				in2ButNot1.push(arr2[j]);
				j++;
			}
		}
		if (i != arr1.length || j != arr2.length) {
			console.error("not all the things were consumed");
			console.error("arr1.length=" + arr1.length + ", i=" + i);
			console.error("arr2.length=" + arr2.length + ", j=" + j);
		}
		return {
			in1ButNot2: in1ButNot2,
			in2ButNot1: in2ButNot1
		};
	}
});

L.Control.BoxSelector = L.Control.extend({

	options: {
		highlightOnDrag: true //on by default but could cause poor performance if there is a large number of markers
	},

	initialize: function (options) {
		L.setOptions(this, options);
		if (this.options.actions == null) {
			//set default action
			this.options.actions = {
				alert: {
					display: "Display selected coords",
					action:	L.Control.BoxSelector.Actions.Alert
				}
			}
		}
	},

	onAdd: function (map) {
		this._map = map;
		this._mapcontainer = map._container;
		this._pane = map._panes.overlayPane;
		this._addHooks();
		this._manager = new SelectionManager(this._map);

		//set up select icon
		this._selectorContainer = L.DomUtil.create('div', 'leaflet-control-box-selector leaflet-bar leaflet-control boxselector-control boxselector-hidden');
		this._buttonBar = L.DomUtil.create('div', 'leaflet-bar-part leaflet-bar-part-single boxselector-button-bar', this._selectorContainer);
		this._toggleElement = L.DomUtil.create('a', 'boxselector-icon', this._buttonBar);
		this._toggleElement.id = 'boxselector-icon';
		this._toggleElement.href = '#';
		var iconWrapper = L.DomUtil.create('div', 'icon-wrapper', this._toggleElement);
		var icon = L.DomUtil.create('img', 'icon', iconWrapper);
		icon.src = '../src/select-icon.png';

		//set up dropdown icon
		var dropdownButton = L.DomUtil.create('a', 'boxselector-dropdown-button', this._buttonBar);
		dropdownButton.id = 'boxselector-dropdown-button';
		dropdownButton.href = '#';
		var dropdownIconWrapper = L.DomUtil.create('div', 'boxselector-dropdown-icon-wrapper', dropdownButton);
		var dropdownIcon = L.DomUtil.create('div', 'boxselector-dropdown-icon', dropdownIconWrapper);
		L.DomEvent.on(dropdownButton, 'click', this._expandCollapse, this)

		//set up drop down (hidden to start with)
		this._expanded = false;
		this._dropdown = L.DomUtil.create('div', 'boxselector-dropdown', this._selectorContainer);
		for (var actionId in this.options.actions) {
			var dropdownEntry = L.DomUtil.create('a', 'boxselector-dropdown-entry', this._dropdown);
			dropdownEntry.actionId = actionId;
			dropdownEntry.href = '#';
			var label = L.DomUtil.create('span', '', dropdownEntry);
			label.innerHTML = this.options.actions[actionId].display;
			label.actionId = actionId;

			L.DomEvent.on(dropdownEntry, 'click', this._actionItemClick, this);
		}

		L.DomEvent.on(this._toggleElement, 'click', this._toggle, this);

		//ensure we tear down
		this._map.on('unload', this._removeHooks, this);

		return this._selectorContainer;
	},

	_expandCollapse: function() {
		if (this._expanded) {
			L.DomUtil.addClass(this._selectorContainer, 'boxselector-hidden');
			L.DomUtil.removeClass(this._selectorContainer, 'boxselector-expanded');
			this._expanded = false;
		} else {
			L.DomUtil.addClass(this._selectorContainer, 'boxselector-expanded');
			L.DomUtil.removeClass(this._selectorContainer, 'boxselector-hidden');
			this._expanded = true;
		}
	},

	_actionItemClick: function(e) {
		var actionItemElement = e.target;
		var actionId = actionItemElement.actionId;
		var action = this.options.actions[actionId].action;
		this._expandCollapse();
		action(this.getSelectedMarkers());
	},

	_addHooks: function () {
		L.DomEvent.on(this._mapcontainer, 'mousedown', this._onMouseDown, this);
	},

	_removeHooks: function () {
		L.DomEvent.off(this._mapcontainer, 'mousedown', this._onMouseDown, this);
	},

	_toggle: function() {
		this._setEnabled(!this._isEnabled());
	},

	_isEnabled: function() {
		return this._selectionEnabled;
	},

	_setEnabled: function(enabled) {
		this._selectionEnabled = enabled;
		if (enabled) {
			//enable selection
			this._map.dragging.disable();
			L.DomUtil.addClass(this._buttonBar, 'selection-enabled');
			this._manager.enable();
		} else {
			//disable selection
			this._map.dragging.enable();
			L.DomUtil.removeClass(this._buttonBar, 'selection-enabled');
			this._manager.disable();
		}
	},

	_resetState: function () {
		this._moved = false;
	},

	_onMouseDown: function (e) {
		if (!this._isEnabled() || (e.button !== 0)) {
			return false;
		}

		this._resetState();

		L.DomUtil.disableTextSelection();
		L.DomUtil.disableImageDrag();

		this._startPoint = this._map.mouseEventToContainerPoint(e);

		L.DomEvent.on(document, {
			contextmenu: L.DomEvent.stop,
			mousemove: this._onMouseMove,
			mouseup: this._onMouseUp,
			keydown: this._onKeyDown
		}, this);

		this._manager.start();
	},

	_onMouseMove: function (e) {
		if (!this._moved) {
			this._moved = true;

			this._box = L.DomUtil.create('div', 'leaflet-zoom-box', this._mapcontainer);
			L.DomUtil.addClass(this._mapcontainer, 'leaflet-crosshair');
		}

		this._point = this._map.mouseEventToContainerPoint(e);

		var bounds = new L.Bounds(this._point, this._startPoint),
		    size = bounds.getSize();

		L.DomUtil.setPosition(this._box, bounds.min);

		this._box.style.width  = size.x + 'px';
		this._box.style.height = size.y + 'px';

		if (this.options.highlightOnDrag) {
			var latLngBounds = new L.LatLngBounds(
				this._map.containerPointToLatLng(this._startPoint),
				this._map.containerPointToLatLng(this._point));

			this._manager.update(latLngBounds);
		}
	},

	_finish: function () {
		if (this._moved) {
			L.DomUtil.remove(this._box);
			L.DomUtil.removeClass(this._mapcontainer, 'leaflet-crosshair');
		}

		L.DomUtil.enableTextSelection();
		L.DomUtil.enableImageDrag();

		L.DomEvent.off(document, {
			contextmenu: L.DomEvent.stop,
			mousemove: this._onMouseMove,
			mouseup: this._onMouseUp,
			keydown: this._onKeyDown
		}, this);
	},

	_onMouseUp: function (e) {
		if ((e.which !== 1) && (e.button !== 1)) { return; }

		this._finish();

		if (!this._moved) { return; }
		// Postpone to next JS tick so internal click event handling
		// still see it as "moved".
		setTimeout(L.bind(this._resetState, this), 0);

		var bounds = new L.LatLngBounds(
			this._map.containerPointToLatLng(this._startPoint),
			this._map.containerPointToLatLng(this._point));

		if (!this.options.highlightOnDrag) {
			//if we aren't highlighting on drag, fool the manager into updating
			this._manager.update(bounds);
		}
		this._manager.finish(bounds);
	},

	getSelectedMarkers: function() {
		return this._manager.getSelectedMarkers();
	},

	_onKeyDown: function (e) {
		if (e.keyCode === 27) {
			this._finish();
		}
	}
});
L.Control.BoxSelector.Actions = {};
L.Control.BoxSelector.Actions.Alert = function(selectedMarkers) {
	var output = "";
	for (var i = 0; i < selectedMarkers.length; i++) {
		var marker = selectedMarkers[i];
		output += marker.name
		output += ": ";
		output += marker.getLatLng().lat;
		output += ",";
		output += marker.getLatLng().lng;
		output += "\n";
	}
	alert(output);
}
<|endoftext|>