<commit_before>import React from 'react';

import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import { formatPercentage } from 'common/format';
import RESOURCE_TYPES from 'game/RESOURCE_TYPES';
import ISSUE_IMPORTANCE from 'parser/core/ISSUE_IMPORTANCE';
import StatisticBox, { STATISTIC_ORDER } from 'interface/others/StatisticBox';
import { TooltipElement } from 'common/Tooltip';
import AbilityTracker from 'parser/shared/modules/AbilityTracker';
import SpellUsable from 'parser/shared/modules/SpellUsable';
import DeathTracker from 'parser/shared/modules/DeathTracker';
import SpellManaCost from 'parser/shared/modules/SpellManaCost';
import Analyzer from 'parser/core/Analyzer';
import ArcaneChargeTracker from './ArcaneChargeTracker';

const debug = false;
const MANA_THRESHOLD = 0.40;

const UNACCEPTABLE_ARCANE_POWER_SPELLS = [
  SPELLS.ARCANE_BARRAGE.id,
  SPELLS.ARCANE_FAMILIAR_TALENT.id,
  SPELLS.ARCANE_INTELLECT.id,
  SPELLS.EVOCATION.id,
  SPELLS.SUPERNOVA_TALENT.id,
  SPELLS.MIRROR_IMAGE_TALENT.id,
  SPELLS.NETHER_TEMPEST_TALENT.id,
  SPELLS.ARCANE_ORB_TALENT.id,
  SPELLS.RUNE_OF_POWER_TALENT.id,
];

class ArcanePower extends Analyzer {
  static dependencies = {
    abilityTracker: AbilityTracker,
    arcaneChargeTracker: ArcaneChargeTracker,
    spellUsable: SpellUsable,
    deathTracker: DeathTracker,
    // Needed for the `resourceCost` prop of events
    spellManaCost: SpellManaCost,
  };

  badUses = 0;
  totalCastsDuringAP = 0;
  badCastsDuringAP = 0;
  runeTimestamp = 0;
  outOfMana = 0;
  buffEndTimestamp = 0;
  arcanePowerOnKill = false;
  arcanePowerCasted = false;
  lowManaCast = 0;
  lowChargesCast = 0;
  noRuneCast = 0;
  delayedRuneCast = 0;

  constructor(...args) {
    super(...args);
    this.hasRuneOfPower = this.selectedCombatant.hasTalent(SPELLS.RUNE_OF_POWER_TALENT.id);
    this.hasOverpowered = this.selectedCombatant.hasTalent(SPELLS.OVERPOWERED_TALENT.id);
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.ARCANE_POWER.id && spellId !== SPELLS.RUNE_OF_POWER_TALENT.id && !this.selectedCombatant.hasBuff(SPELLS.ARCANE_POWER.id)) {
      return;
    }
    if (spellId === SPELLS.RUNE_OF_POWER_TALENT.id) {
      this.runeTimestamp = event.timestamp;
    } else if (spellId === SPELLS.ARCANE_POWER.id) {
      const currentManaPercent = event.classResources[0].amount / event.classResources[0].max;
      this.arcanePowerCasted = true;

      if (this.arcaneChargeTracker.charges < 4 || (this.hasRuneOfPower && event.timestamp - this.runeTimestamp > 200) || (!this.hasOverpowered && currentManaPercent < MANA_THRESHOLD)) {
        this.badUses += 1;
      }

      if (this.selectedCombatant.hasBuff(SPELLS.ARCANE_POWER.id)) {
        this.buffEndTimestamp = event.timestamp + 10000;
        debug && this.log('Arcane Power Cast During Arcane Power Proc');
        debug && this.log('Arcane Power End Time adjusted to');
      }

      if (this.arcaneChargeTracker.charges < 4) {
        debug && this.log('Arcane Power Cast with Low Charges');
        debug && this.log('Arcane Charges: ' + this.arcaneChargeTracker.charges);
        this.lowChargesCast += 1;
      }

      if (this.hasRuneOfPower && event.timestamp - this.runeTimestamp > 10000) {
        debug && this.log('Arcane Power Cast without Rune of Power');
        this.noRuneCast += 1;
      } else if (this.hasRuneOfPower && event.timestamp - this.runeTimestamp > 200) {
        debug && this.log('Arcane Power Cast delayed after Rune of Power');
        debug && this.log('Arcane Power Delay: ' + (event.timestamp - this.runeTimestamp));
        this.delayedRuneCast += 1;
      }

      if (!this.hasOverpowered && currentManaPercent < MANA_THRESHOLD) {
        debug && this.log('Arcane Power Cast with low mana');
        debug && !this.hasOverpowered && this.log('Mana Percent: ' + currentManaPercent);
        this.lowManaCast += 1;
      }
    } else {
      this.totalCastsDuringAP += 1;
      if (UNACCEPTABLE_ARCANE_POWER_SPELLS.includes(spellId)) {
        debug && this.log('Cast ' + event.ability.name + ' during Arcane Power');
        this.badCastsDuringAP += 1;
      } else if (spellId === SPELLS.ARCANE_BLAST.id || spellId === SPELLS.ARCANE_EXPLOSION.id) {
        const manaInfo = event.classResources.find(resource => resource.type === RESOURCE_TYPES.MANA.id);
        if (!manaInfo) {
          return;
        }
        const currentMana = manaInfo.amount;
        const manaCost = (event.resourceCost[RESOURCE_TYPES.MANA.id] + (event.resourceCost[RESOURCE_TYPES.MANA.id] * this.arcaneChargeTracker.charges));
        const manaRemaining = currentMana - manaCost;
        const buffTimeRemaining = this.buffEndTimestamp - event.timestamp;
        if (manaRemaining < this.estimatedManaCost(spellId) && buffTimeRemaining > 1000) {
          debug && this.log('Ran Out of Mana during Arcane Power');
          debug && this.log('Mana Remaining: ' + manaRemaining);
          debug && this.log('Estimated Mana Cost: ' + this.estimatedManaCost(spellId));
          debug && this.log('Time left on Arcane Power: ' + buffTimeRemaining);
          this.outOfMana += 1;
        }
      }
    }
  }

  on_toPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.ARCANE_POWER.id) {
      return;
    }

    if (this.arcanePowerCasted) {
      debug && this.log('Arcane Power Cast');
      this.buffEndTimestamp = event.timestamp + 10000;
      debug && this.log('Arcane Power Ends');
    } else {
      debug && this.log('Arcane Power Proc');
      this.buffEndTimestamp = event.timestamp + 8000;
      debug && this.log('Arcane Power Ends');
    }
  }

  on_toPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.ARCANE_POWER.id) {
      return;
    }
    this.arcanePowerCasted = false;
  }

  on_fightend() {
    if (this.spellUsable.isAvailable(SPELLS.ARCANE_POWER.id) && this.deathTracker.isAlive) {
      this.arcanePowerOnKill = true;
    }
  }

  estimatedManaCost(spellId) {
    if (spellId === SPELLS.ARCANE_EXPLOSION.id) {
      if (this.hasOverpowered) {
        return 2000 * 0.4;
      } else {
        return 2000;
      }
    } else if (spellId === SPELLS.ARCANE_BLAST.id) {
      if (this.hasOverpowered) {
        return (550 + (550 * this.arcaneChargeTracker.charges)) * 0.4;
      } else if (this.selectedCombatant.hasBuff(SPELLS.RULE_OF_THREES_BUFF.id)) {
        return 0;
      } else {
        return 550 + (550 * this.arcaneChargeTracker.charges);
      }
    }
    return 0;
  }

  get requiredChecks() {
    let checks = 1;
    if (!this.hasOverpowered) {
      //Also checks mana level if you dont have Overpowered talented
      checks += 1;
    }
    if (this.hasRuneOfPower) {
      //Also checks to see if RoP was delayed or missing if RoP is talented
      checks += 1;
    }
    return checks;

  }

  get failedChecks() {
    return this.lowChargesCast + this.lowManaCast + this.noRuneCast + this.delayedRuneCast;
  }

  get cooldownUtilization() {
    return 1 - (this.failedChecks / (this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts * this.requiredChecks));
  }

  get castUtilization() {
    return 1 - (this.badCastsDuringAP / this.totalCastsDuringAP);
  }

  get manaUtilization() {
    return 1 - (this.outOfMana / this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts);
  }

  get cooldownSuggestionThresholds() {
    return {
      actual: this.cooldownUtilization,
      isLessThan: {
        minor: 1,
        average: 0.80,
        major: 0.60,
      },
      style: 'percentage',
    };
  }

  get castSuggestionThresholds() {
    return {
      actual: this.castUtilization,
      isLessThan: {
        minor: 1,
        average: 0.95,
        major: 0.90,
      },
      style: 'percentage',
    };
  }

  get manaUtilizationThresholds() {
    return {
      actual: this.manaUtilization,
      isLessThan: {
        minor: 1,
        average: 0.95,
        major: 0.90,
      },
      style: 'percentage',
    };
  }

  get arcanePowerOnKillSuggestionThresholds() {
    return {
      actual: this.arcanePowerOnKill,
      isEqual: true,
      style: 'boolean',
    };
  }

  suggestions(when) {
    when(this.cooldownSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<>You cast <SpellLink id={SPELLS.ARCANE_POWER.id} /> without proper setup {this.badUses} times. Arcane Power has a short duration so you should get the most out of it by meeting all requirements before casting it.

You have 4 <SpellLink id={SPELLS.ARCANE_CHARGE.id} /> - You had this {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts - this.lowChargesCast} out of {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts} casts.

          {this.hasRuneOfPower ?
You cast <SpellLink id={SPELLS.RUNE_OF_POWER_TALENT.id} /> <TooltipElement content="Arcane Power should be cast right on the end of the Rune of Power cast. There should not be any casts or any delay in between Rune of Power and Arcane Power to ensure that Rune of Power is up for the entire duration of Arcane Power.">immediately</TooltipElement> before Arcane Power - You did this {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts - (this.delayedRuneCast + this.noRuneCast)} out of {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts} casts.
 : ''}
          {!this.hasOverpowered ?
You have more than 40% mana - You had this {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts - this.lowManaCast} out of {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts} casts.
 : ''}

        </>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .actual(`${this.badUses} Bad Casts`)
          .recommended(`0 is recommended`);
      });
    when(this.castSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<>You cast spells other than <SpellLink id={SPELLS.ARCANE_BLAST.id} />,<SpellLink id={SPELLS.ARCANE_MISSILES.id} />, <SpellLink id={SPELLS.ARCANE_EXPLOSION.id} />, and <SpellLink id={SPELLS.PRESENCE_OF_MIND.id} /> during <SpellLink id={SPELLS.ARCANE_POWER.id} />. Arcane Power is a short duration, so you should ensure that you are getting the most use out of it. Buff spells like Rune of Power should be cast immediately before casting Arcane Power. Other spells such as Charged Up, Blink/Shimmer, etc are acceptable during Arcane Power, but should be avoided if possible.</>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .actual(`${formatPercentage(this.castUtilization)}% Utilization`)
          .recommended(`${formatPercentage(recommended)}% is recommended`);
      });
    when(this.manaUtilizationThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<>You ran dangerously low or ran out of mana during <SpellLink id={SPELLS.ARCANE_POWER.id} /> {this.outOfMana} times. Running out of mana during Arcane Power is a massive DPS loss and should be avoided at all costs. {!this.hasOverpowered ? 'To avoid this, ensure you have at least 40% mana before casting Arcane Power to ensure you have enough mana to finish Arcane Power.' : ''}</>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .actual(`${formatPercentage(this.manaUtilization)}% Utilization`)
          .recommended(`${formatPercentage(recommended)}% is recommended`);
      });
    when(this.arcanePowerOnKillSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<><SpellLink id={SPELLS.ARCANE_POWER.id} /> was available to be cast when the boss died. You should ensure that you are casting Arcane Power on cooldown, especially at the end of the fight to get a little bit of last minute damage into the boss.</>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .staticImportance(ISSUE_IMPORTANCE.REGULAR);
      });
  }

  statistic() {
    return (
      <StatisticBox
        position={STATISTIC_ORDER.CORE(15)}
        icon={<SpellIcon id={SPELLS.ARCANE_POWER.id} />}
        value={(<>
          <SpellIcon
            id={SPELLS.ARCANE_POWER.id}
            style={{
              height: '1.2em',
              marginBottom: '.15em',
            }}
          />
          {' '}{formatPercentage(this.cooldownUtilization, 0)} %

          <SpellIcon
            id={SPELLS.ARCANE_BLAST.id}
            style={{
              height: '1.2em',
              marginBottom: '.15em',
            }}
          />
          {' '}{formatPercentage(this.castUtilization, 0)} %
        </>)}
        label="Arcane Power Utilization"
        tooltip={(
          <>
            Before casting Arcane Power, you should ensure that you meet all of the following requirements. If Arcane Power frequently comes off cooldown and these requirements are not already met, then consider modifying your rotation to ensure that they are met before Arcane Power comes off cooldown

You have 4 Arcane Charges - Missed {this.lowChargesCast} times

              {this.hasRuneOfPower &&
You cast Rune of Power immediately before AP - Missed {this.noRuneCast + this.delayedRuneCast} times
}
              {!this.hasOverpowered &&
You have more than 40% mana - Missed {this.lowManaCast} times
}

            Additionally, you should only be casting Arcane Blast and Arcane Missiles (If you have a Clearcasting Proc) during Arcane Power to maximize the short cooldown duration.
          </>
        )}
      />
    );
  }
}

export default ArcanePower;
<commit_msg>Fix crash in ArcanePower analyzer<commit_after>import React from 'react';

import SPELLS from 'common/SPELLS';
import SpellLink from 'common/SpellLink';
import SpellIcon from 'common/SpellIcon';
import { formatPercentage } from 'common/format';
import RESOURCE_TYPES from 'game/RESOURCE_TYPES';
import ISSUE_IMPORTANCE from 'parser/core/ISSUE_IMPORTANCE';
import StatisticBox, { STATISTIC_ORDER } from 'interface/others/StatisticBox';
import { TooltipElement } from 'common/Tooltip';
import AbilityTracker from 'parser/shared/modules/AbilityTracker';
import SpellUsable from 'parser/shared/modules/SpellUsable';
import DeathTracker from 'parser/shared/modules/DeathTracker';
import SpellManaCost from 'parser/shared/modules/SpellManaCost';
import Analyzer from 'parser/core/Analyzer';
import ArcaneChargeTracker from './ArcaneChargeTracker';

const debug = false;
const MANA_THRESHOLD = 0.40;

const UNACCEPTABLE_ARCANE_POWER_SPELLS = [
  SPELLS.ARCANE_BARRAGE.id,
  SPELLS.ARCANE_FAMILIAR_TALENT.id,
  SPELLS.ARCANE_INTELLECT.id,
  SPELLS.EVOCATION.id,
  SPELLS.SUPERNOVA_TALENT.id,
  SPELLS.MIRROR_IMAGE_TALENT.id,
  SPELLS.NETHER_TEMPEST_TALENT.id,
  SPELLS.ARCANE_ORB_TALENT.id,
  SPELLS.RUNE_OF_POWER_TALENT.id,
];

class ArcanePower extends Analyzer {
  static dependencies = {
    abilityTracker: AbilityTracker,
    arcaneChargeTracker: ArcaneChargeTracker,
    spellUsable: SpellUsable,
    deathTracker: DeathTracker,
    // Needed for the `resourceCost` prop of events
    spellManaCost: SpellManaCost,
  };

  badUses = 0;
  totalCastsDuringAP = 0;
  badCastsDuringAP = 0;
  runeTimestamp = 0;
  outOfMana = 0;
  buffEndTimestamp = 0;
  arcanePowerOnKill = false;
  arcanePowerCasted = false;
  lowManaCast = 0;
  lowChargesCast = 0;
  noRuneCast = 0;
  delayedRuneCast = 0;

  constructor(...args) {
    super(...args);
    this.hasRuneOfPower = this.selectedCombatant.hasTalent(SPELLS.RUNE_OF_POWER_TALENT.id);
    this.hasOverpowered = this.selectedCombatant.hasTalent(SPELLS.OVERPOWERED_TALENT.id);
  }

  on_byPlayer_cast(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.ARCANE_POWER.id && spellId !== SPELLS.RUNE_OF_POWER_TALENT.id && !this.selectedCombatant.hasBuff(SPELLS.ARCANE_POWER.id)) {
      return;
    }
    if (spellId === SPELLS.RUNE_OF_POWER_TALENT.id) {
      this.runeTimestamp = event.timestamp;
    } else if (spellId === SPELLS.ARCANE_POWER.id) {
      const currentManaPercent = event.classResources[0].amount / event.classResources[0].max;
      this.arcanePowerCasted = true;

      if (this.arcaneChargeTracker.charges < 4 || (this.hasRuneOfPower && event.timestamp - this.runeTimestamp > 200) || (!this.hasOverpowered && currentManaPercent < MANA_THRESHOLD)) {
        this.badUses += 1;
      }

      if (this.selectedCombatant.hasBuff(SPELLS.ARCANE_POWER.id)) {
        this.buffEndTimestamp = event.timestamp + 10000;
        debug && this.log('Arcane Power Cast During Arcane Power Proc');
        debug && this.log('Arcane Power End Time adjusted to');
      }

      if (this.arcaneChargeTracker.charges < 4) {
        debug && this.log('Arcane Power Cast with Low Charges');
        debug && this.log('Arcane Charges: ' + this.arcaneChargeTracker.charges);
        this.lowChargesCast += 1;
      }

      if (this.hasRuneOfPower && event.timestamp - this.runeTimestamp > 10000) {
        debug && this.log('Arcane Power Cast without Rune of Power');
        this.noRuneCast += 1;
      } else if (this.hasRuneOfPower && event.timestamp - this.runeTimestamp > 200) {
        debug && this.log('Arcane Power Cast delayed after Rune of Power');
        debug && this.log('Arcane Power Delay: ' + (event.timestamp - this.runeTimestamp));
        this.delayedRuneCast += 1;
      }

      if (!this.hasOverpowered && currentManaPercent < MANA_THRESHOLD) {
        debug && this.log('Arcane Power Cast with low mana');
        debug && !this.hasOverpowered && this.log('Mana Percent: ' + currentManaPercent);
        this.lowManaCast += 1;
      }
    } else {
      this.totalCastsDuringAP += 1;
      if (UNACCEPTABLE_ARCANE_POWER_SPELLS.includes(spellId)) {
        debug && this.log('Cast ' + event.ability.name + ' during Arcane Power');
        this.badCastsDuringAP += 1;
      } else if (spellId === SPELLS.ARCANE_BLAST.id || spellId === SPELLS.ARCANE_EXPLOSION.id) {
        event.classResources && event.classResources.forEach(resource => {
          if (resource.type !== RESOURCE_TYPES.MANA.id) {
            return;
          }
          const currentMana = resource.amount;
          const manaCost = (event.resourceCost[RESOURCE_TYPES.MANA.id] + (event.resourceCost[RESOURCE_TYPES.MANA.id] * this.arcaneChargeTracker.charges));
          const manaRemaining = currentMana - manaCost;
          const buffTimeRemaining = this.buffEndTimestamp - event.timestamp;
          if (manaRemaining < this.estimatedManaCost(spellId) && buffTimeRemaining > 1000) {
            debug && this.log('Ran Out of Mana during Arcane Power');
            debug && this.log('Mana Remaining: ' + manaRemaining);
            debug && this.log('Estimated Mana Cost: ' + this.estimatedManaCost(spellId));
            debug && this.log('Time left on Arcane Power: ' + buffTimeRemaining);
            this.outOfMana += 1;
          }
        });
      }
    }
  }

  on_toPlayer_applybuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.ARCANE_POWER.id) {
      return;
    }

    if (this.arcanePowerCasted) {
      debug && this.log('Arcane Power Cast');
      this.buffEndTimestamp = event.timestamp + 10000;
      debug && this.log('Arcane Power Ends');
    } else {
      debug && this.log('Arcane Power Proc');
      this.buffEndTimestamp = event.timestamp + 8000;
      debug && this.log('Arcane Power Ends');
    }
  }

  on_toPlayer_removebuff(event) {
    const spellId = event.ability.guid;
    if (spellId !== SPELLS.ARCANE_POWER.id) {
      return;
    }
    this.arcanePowerCasted = false;
  }

  on_fightend() {
    if (this.spellUsable.isAvailable(SPELLS.ARCANE_POWER.id) && this.deathTracker.isAlive) {
      this.arcanePowerOnKill = true;
    }
  }

  estimatedManaCost(spellId) {
    if (spellId === SPELLS.ARCANE_EXPLOSION.id) {
      if (this.hasOverpowered) {
        return 2000 * 0.4;
      } else {
        return 2000;
      }
    } else if (spellId === SPELLS.ARCANE_BLAST.id) {
      if (this.hasOverpowered) {
        return (550 + (550 * this.arcaneChargeTracker.charges)) * 0.4;
      } else if (this.selectedCombatant.hasBuff(SPELLS.RULE_OF_THREES_BUFF.id)) {
        return 0;
      } else {
        return 550 + (550 * this.arcaneChargeTracker.charges);
      }
    }
    return 0;
  }

  get requiredChecks() {
    let checks = 1;
    if (!this.hasOverpowered) {
      //Also checks mana level if you dont have Overpowered talented
      checks += 1;
    }
    if (this.hasRuneOfPower) {
      //Also checks to see if RoP was delayed or missing if RoP is talented
      checks += 1;
    }
    return checks;

  }

  get failedChecks() {
    return this.lowChargesCast + this.lowManaCast + this.noRuneCast + this.delayedRuneCast;
  }

  get cooldownUtilization() {
    return 1 - (this.failedChecks / (this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts * this.requiredChecks));
  }

  get castUtilization() {
    return 1 - (this.badCastsDuringAP / this.totalCastsDuringAP);
  }

  get manaUtilization() {
    return 1 - (this.outOfMana / this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts);
  }

  get cooldownSuggestionThresholds() {
    return {
      actual: this.cooldownUtilization,
      isLessThan: {
        minor: 1,
        average: 0.80,
        major: 0.60,
      },
      style: 'percentage',
    };
  }

  get castSuggestionThresholds() {
    return {
      actual: this.castUtilization,
      isLessThan: {
        minor: 1,
        average: 0.95,
        major: 0.90,
      },
      style: 'percentage',
    };
  }

  get manaUtilizationThresholds() {
    return {
      actual: this.manaUtilization,
      isLessThan: {
        minor: 1,
        average: 0.95,
        major: 0.90,
      },
      style: 'percentage',
    };
  }

  get arcanePowerOnKillSuggestionThresholds() {
    return {
      actual: this.arcanePowerOnKill,
      isEqual: true,
      style: 'boolean',
    };
  }

  suggestions(when) {
    when(this.cooldownSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<>You cast <SpellLink id={SPELLS.ARCANE_POWER.id} /> without proper setup {this.badUses} times. Arcane Power has a short duration so you should get the most out of it by meeting all requirements before casting it.

You have 4 <SpellLink id={SPELLS.ARCANE_CHARGE.id} /> - You had this {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts - this.lowChargesCast} out of {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts} casts.

          {this.hasRuneOfPower ?
You cast <SpellLink id={SPELLS.RUNE_OF_POWER_TALENT.id} /> <TooltipElement content="Arcane Power should be cast right on the end of the Rune of Power cast. There should not be any casts or any delay in between Rune of Power and Arcane Power to ensure that Rune of Power is up for the entire duration of Arcane Power.">immediately</TooltipElement> before Arcane Power - You did this {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts - (this.delayedRuneCast + this.noRuneCast)} out of {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts} casts.
 : ''}
          {!this.hasOverpowered ?
You have more than 40% mana - You had this {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts - this.lowManaCast} out of {this.abilityTracker.getAbility(SPELLS.ARCANE_POWER.id).casts} casts.
 : ''}

        </>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .actual(`${this.badUses} Bad Casts`)
          .recommended(`0 is recommended`);
      });
    when(this.castSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<>You cast spells other than <SpellLink id={SPELLS.ARCANE_BLAST.id} />,<SpellLink id={SPELLS.ARCANE_MISSILES.id} />, <SpellLink id={SPELLS.ARCANE_EXPLOSION.id} />, and <SpellLink id={SPELLS.PRESENCE_OF_MIND.id} /> during <SpellLink id={SPELLS.ARCANE_POWER.id} />. Arcane Power is a short duration, so you should ensure that you are getting the most use out of it. Buff spells like Rune of Power should be cast immediately before casting Arcane Power. Other spells such as Charged Up, Blink/Shimmer, etc are acceptable during Arcane Power, but should be avoided if possible.</>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .actual(`${formatPercentage(this.castUtilization)}% Utilization`)
          .recommended(`${formatPercentage(recommended)}% is recommended`);
      });
    when(this.manaUtilizationThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<>You ran dangerously low or ran out of mana during <SpellLink id={SPELLS.ARCANE_POWER.id} /> {this.outOfMana} times. Running out of mana during Arcane Power is a massive DPS loss and should be avoided at all costs. {!this.hasOverpowered ? 'To avoid this, ensure you have at least 40% mana before casting Arcane Power to ensure you have enough mana to finish Arcane Power.' : ''}</>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .actual(`${formatPercentage(this.manaUtilization)}% Utilization`)
          .recommended(`${formatPercentage(recommended)}% is recommended`);
      });
    when(this.arcanePowerOnKillSuggestionThresholds)
      .addSuggestion((suggest, actual, recommended) => {
        return suggest(<><SpellLink id={SPELLS.ARCANE_POWER.id} /> was available to be cast when the boss died. You should ensure that you are casting Arcane Power on cooldown, especially at the end of the fight to get a little bit of last minute damage into the boss.</>)
          .icon(SPELLS.ARCANE_POWER.icon)
          .staticImportance(ISSUE_IMPORTANCE.REGULAR);
      });
  }

  statistic() {
    return (
      <StatisticBox
        position={STATISTIC_ORDER.CORE(15)}
        icon={<SpellIcon id={SPELLS.ARCANE_POWER.id} />}
        value={(<>
          <SpellIcon
            id={SPELLS.ARCANE_POWER.id}
            style={{
              height: '1.2em',
              marginBottom: '.15em',
            }}
          />
          {' '}{formatPercentage(this.cooldownUtilization, 0)} %

          <SpellIcon
            id={SPELLS.ARCANE_BLAST.id}
            style={{
              height: '1.2em',
              marginBottom: '.15em',
            }}
          />
          {' '}{formatPercentage(this.castUtilization, 0)} %
        </>)}
        label="Arcane Power Utilization"
        tooltip={(
          <>
            Before casting Arcane Power, you should ensure that you meet all of the following requirements. If Arcane Power frequently comes off cooldown and these requirements are not already met, then consider modifying your rotation to ensure that they are met before Arcane Power comes off cooldown

You have 4 Arcane Charges - Missed {this.lowChargesCast} times

              {this.hasRuneOfPower &&
You cast Rune of Power immediately before AP - Missed {this.noRuneCast + this.delayedRuneCast} times
}
              {!this.hasOverpowered &&
You have more than 40% mana - Missed {this.lowManaCast} times
}

            Additionally, you should only be casting Arcane Blast and Arcane Missiles (If you have a Clearcasting Proc) during Arcane Power to maximize the short cooldown duration.
          </>
        )}
      />
    );
  }
}

export default ArcanePower;
<|endoftext|>


<commit_before>/**
 * 通用工具类
 */
(function() {

    /**
     * 地址栏获取参数
     * @param name 参数名称
     * @return {*}
     */
    function getParameter(name) {
        try {
            var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
            var r = window.location.search.substr(1).match(reg);
            if (r != null) {
                return decodeURI(r[2]);
            }
            return null;
        } catch (e) {
            log(e.message);
        }
    }
    //树形数据组织
    function treeData (source, id, parentId, children) {
        let cloneData = JSON.parse(JSON.stringify(source))
        return cloneData.filter(father => {
            let branchArr = cloneData.filter(child => father[id] == child[parentId]);
            branchArr.length > 0 ? father[children] = branchArr : ''
            return !father[parentId]        // 如果第一层不是parentId=0，请自行修改
        })
    }

    //日期处理
    var date = {
        //格式化时间
        fmt: function(date, fmt) {
            var date = new Date(date);
            log(fmt);
            if (!fmt) {
                fmt = "yyyy-mm-dd";
            }
            var o = {
                "M+": date.getMonth() + 1, //月份
                "d+": date.getDate(), //日
                "h+": date.getHours(), //小时
                "m+": date.getMinutes(), //分
                "s+": date.getSeconds(), //秒
                "q+": Math.floor((date.getMonth() + 3) / 3), //季度
                "S": date.getMilliseconds() //毫秒
            };
            if (/(y+)/.test(fmt))
                fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
            for (var k in o)
                if (new RegExp("(" + k + ")").test(fmt))
                    fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
            return fmt;
        },
        diyFmt: function (time) {
            //如果传进来的是字符串，转成时间
            if(Object.prototype.toString.call(new Date()) != Object.prototype.toString.call(time) ){
                time = new Date(time);
            }
            var nowDate = new Date().getTime();
            var dif = (nowDate - time)/1000;
            //时间字符串
            var timestr ="";
            if(dif<60){
                timestr = "刚刚";
            }else if(dif<3600){
                timestr = moment(time).format('A') + moment(time).format('H:mm');
            }else if(dif<86400){
                timestr = "昨天";
            }else if(dif<172800){
                timestr = moment(time).format("dddd");
            }else if(dif<31536000){
                timestr = moment(time).format("MMM Do").replace(" ","");
            }else{
                timestr = moment(time).subtract(10, 'days').calendar();
            }
            return timestr;
        }
    }

    var array = {

        //根据key清理arr里面重复的值
        unique: function(arr, key) {
            if (arr.length == 0) {
                return;
            }
            var result = [arr[0]];
            for (var i = 1; i < arr.length; i++) {
                var item = arr[i];
                var repeat = false;
                var repeat = false;
                for (var j = 0; j < result.length; j++) {
                    if (item[key] == result[j][key]) {
                        if (item['write'] && result[j]['write'] == false) {
                            break;
                        }
                        repeat = true;
                        break;
                    }
                }
                if (!repeat) {
                    result.push(item);
                }
            }
            return result;
        },
        //根据pro属性与value移除arr对应的值
        remove: function(arr, attr, value) {
            for (var j = 0; j < arr.length; j++) {
                if (arr[j][attr] == value) {
                    arr.splice(j, 1);
                    break;
                }
            }
        }
    }

    var convert = {
        byte: function(bytes) {
            if (isNaN(bytes)) {
                return '';
            }
            var symbols = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
            var exp = Math.floor(Math.log(bytes) / Math.log(2));
            if (exp < 1) {
                exp = 0;
            }
            var i = Math.floor(exp / 10);
            bytes = bytes / Math.pow(2, 10 * i);

            if (bytes.toString().length > bytes.toFixed(2).toString().length) {
                bytes = bytes.toFixed(2);
            }
            return bytes + ' ' + symbols[i];
        },

        //根据经纬度计算记录
        distance: function(lon1, lat1, lon2, lat2) {
            var DEF_PI = 3.14159265359; // PI
            var DEF_2PI = 6.28318530712; // 2*PI
            var DEF_PI180 = 0.01745329252; // PI/180.0
            var DEF_R = 6370693.5; // radius of earth

            var ew1, ns1, ew2, ns2;
            var dx, dy, dew;
            var distance;
            // 角度转换为弧度
            ew1 = lon1 * DEF_PI180;
            ns1 = lat1 * DEF_PI180;
            ew2 = lon2 * DEF_PI180;
            ns2 = lat2 * DEF_PI180;
            // 经度差
            dew = ew1 - ew2;
            // 若跨东经和西经180 度，进行调整
            if (dew > DEF_PI)
                dew = DEF_2PI - dew;
            else if (dew < -DEF_PI)
                dew = DEF_2PI + dew;
            dx = DEF_R * Math.cos(ns1) * dew; // 东西方向长度(在纬度圈上的投影长度)
            dy = DEF_R * (ns1 - ns2); // 南北方向长度(在经度圈上的投影长度)
            // 勾股定理求斜边长
            distance = Math.sqrt(dx * dx + dy * dy).toFixed(0);
            return distance;
        }

    }

    var log = function(msg) {
        if (ms.debug) {
            console.log(msg);
        }
    }

    var store = {
        set: function(key, value) {
            window.sessionStorage.setItem(key, value);
        },
        get: function(key) {
            return window.sessionStorage.getItem(key);
        },
        remove: function(key) {
            window.sessionStorage.removeItem(key);
        }
    }

    var cookie = {

    }

    var util = {
        getParameter: getParameter,
        treeData:treeData,
        date: date,
        array: array,
        log: log,
        convert: convert,
        store: store,
    }

    if (typeof ms != "object") {
        window.ms = {};
    }
    window.ms.util = util;
}());<commit_msg>添加自定义模型<commit_after>/**
 * 通用工具类
 */
(function() {

    /**
     * 地址栏获取参数
     * @param name 参数名称
     * @return {*}
     */
    function getParameter(name) {
        try {
            var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)");
            var r = window.location.search.substr(1).match(reg);
            if (r != null) {
                return decodeURI(r[2]);
            }
            return null;
        } catch (e) {
            log(e.message);
        }
    }
    //树形数据组织
    function treeData (source, id, parentId, children) {
        let cloneData = JSON.parse(JSON.stringify(source))
        return cloneData.filter(father => {
            let branchArr = cloneData.filter(child => father[id] == child[parentId]);
            branchArr.length > 0 ? father[children] = branchArr : ''
            return !father[parentId]||father[parentId]=='0'        // 如果第一层不是parentId=0，请自行修改
        })
    }
    //验证是否为子集
    function childValidate (sourceList,id,parentId,key,parentKey){
        var data=sourceList.find(x=>x[key]==parentId);
        if(data&&data[parentKey]!='0'&&data[parentKey]){
            if(id==data[parentKey]){
                return false
            }
            return childValidate(sourceList,id,data[parentKey],key,parentKey)
        }
        return true;
    }

    //日期处理
    var date = {
        //格式化时间
        fmt: function(date, fmt) {
            var date = new Date(date);
            log(fmt);
            if (!fmt) {
                fmt = "yyyy-mm-dd";
            }
            var o = {
                "M+": date.getMonth() + 1, //月份
                "d+": date.getDate(), //日
                "h+": date.getHours(), //小时
                "m+": date.getMinutes(), //分
                "s+": date.getSeconds(), //秒
                "q+": Math.floor((date.getMonth() + 3) / 3), //季度
                "S": date.getMilliseconds() //毫秒
            };
            if (/(y+)/.test(fmt))
                fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
            for (var k in o)
                if (new RegExp("(" + k + ")").test(fmt))
                    fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
            return fmt;
        },
        diyFmt: function (time) {
            //如果传进来的是字符串，转成时间
            if(Object.prototype.toString.call(new Date()) != Object.prototype.toString.call(time) ){
                time = new Date(time);
            }
            var nowDate = new Date().getTime();
            var dif = (nowDate - time)/1000;
            //时间字符串
            var timestr ="";
            if(dif<60){
                timestr = "刚刚";
            }else if(dif<3600){
                timestr = moment(time).format('A') + moment(time).format('H:mm');
            }else if(dif<86400){
                timestr = "昨天";
            }else if(dif<172800){
                timestr = moment(time).format("dddd");
            }else if(dif<31536000){
                timestr = moment(time).format("MMM Do").replace(" ","");
            }else{
                timestr = moment(time).subtract(10, 'days').calendar();
            }
            return timestr;
        }
    }

    var array = {

        //根据key清理arr里面重复的值
        unique: function(arr, key) {
            if (arr.length == 0) {
                return;
            }
            var result = [arr[0]];
            for (var i = 1; i < arr.length; i++) {
                var item = arr[i];
                var repeat = false;
                var repeat = false;
                for (var j = 0; j < result.length; j++) {
                    if (item[key] == result[j][key]) {
                        if (item['write'] && result[j]['write'] == false) {
                            break;
                        }
                        repeat = true;
                        break;
                    }
                }
                if (!repeat) {
                    result.push(item);
                }
            }
            return result;
        },
        //根据pro属性与value移除arr对应的值
        remove: function(arr, attr, value) {
            for (var j = 0; j < arr.length; j++) {
                if (arr[j][attr] == value) {
                    arr.splice(j, 1);
                    break;
                }
            }
        }
    }

    var convert = {
        byte: function(bytes) {
            if (isNaN(bytes)) {
                return '';
            }
            var symbols = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
            var exp = Math.floor(Math.log(bytes) / Math.log(2));
            if (exp < 1) {
                exp = 0;
            }
            var i = Math.floor(exp / 10);
            bytes = bytes / Math.pow(2, 10 * i);

            if (bytes.toString().length > bytes.toFixed(2).toString().length) {
                bytes = bytes.toFixed(2);
            }
            return bytes + ' ' + symbols[i];
        },

        //根据经纬度计算记录
        distance: function(lon1, lat1, lon2, lat2) {
            var DEF_PI = 3.14159265359; // PI
            var DEF_2PI = 6.28318530712; // 2*PI
            var DEF_PI180 = 0.01745329252; // PI/180.0
            var DEF_R = 6370693.5; // radius of earth

            var ew1, ns1, ew2, ns2;
            var dx, dy, dew;
            var distance;
            // 角度转换为弧度
            ew1 = lon1 * DEF_PI180;
            ns1 = lat1 * DEF_PI180;
            ew2 = lon2 * DEF_PI180;
            ns2 = lat2 * DEF_PI180;
            // 经度差
            dew = ew1 - ew2;
            // 若跨东经和西经180 度，进行调整
            if (dew > DEF_PI)
                dew = DEF_2PI - dew;
            else if (dew < -DEF_PI)
                dew = DEF_2PI + dew;
            dx = DEF_R * Math.cos(ns1) * dew; // 东西方向长度(在纬度圈上的投影长度)
            dy = DEF_R * (ns1 - ns2); // 南北方向长度(在经度圈上的投影长度)
            // 勾股定理求斜边长
            distance = Math.sqrt(dx * dx + dy * dy).toFixed(0);
            return distance;
        }

    }

    var log = function(msg) {
        if (ms.debug) {
            console.log(msg);
        }
    }

    var store = {
        set: function(key, value) {
            window.sessionStorage.setItem(key, value);
        },
        get: function(key) {
            return window.sessionStorage.getItem(key);
        },
        remove: function(key) {
            window.sessionStorage.removeItem(key);
        }
    }

    var cookie = {

    }

    var util = {
        getParameter: getParameter,
        treeData:treeData,
        childValidate:childValidate,
        date: date,
        array: array,
        log: log,
        convert: convert,
        store: store,
    }

    if (typeof ms != "object") {
        window.ms = {};
    }
    window.ms.util = util;
}());<|endoftext|>


<commit_before>/*==================================================
 *  Exhibit.HtmlTableImporter
 *==================================================
 */

Exhibit.HtmlTableImporter = {
};
Exhibit.importers["text/html"] = Exhibit.HtmlTableImporter;

Exhibit.HtmlTableImporter.load = function(link, database, cont) {
    var url = typeof link == "string" ? link : link.href;
    if (url.substr(0,1) == "#") {
        try {
            var id = /#(.*)/.exec(f)[1];
            var table = document.getElementById(id);
            table.style.display = "none"; // as we are replacing it with the exhibit UI

            Exhibit.HtmlTableImporter.loadTable(table, database);
        } catch (e) {
            SimileAjax.Debug.exception(e);
        } finally {
            if (cont) {
                cont();
            }
        }
    } else if (typeof link != "string") {
		var xpath = link.getAttribute('ex:xpath');
		var columns = (link.getAttribute('ex:columns')).split(',');
		var babelURL = "http://simile.mit.edu/babel/html-extractor?" + [
			"xpath=" + xpath,
			"url=" + encodeURIComponent(url)
		].join("&");
		var fConvert = function(string) {
			var div = document.createElement("div");
			div.innerHTML = string;
			var table = div.firstChild;

		    var th, ths = table.getElementsByTagName("th");
			for( col = 0; th = ths[col]; col++ ) {
				var label = columns[col];
				th.setAttribute('ex:name', label);
			}

			Exhibit.HtmlTableImporter.loadTable(table, database);
			return {};
		}
        return Exhibit.JSONPImporter.load(babelURL, database, cont, fConvert);
    } else {
        if (cont) {
            cont();
        }
    }
}

Exhibit.HtmlTableImporter.loadTable = function(table, database) {

    var textOf = function( n ) { return n.textContent || n.innerText || ""; };
    var readAttributes = function( node, attributes ) {
        var result = {}, found = false, attr, value, i;
        for( i = 0; attr = attributes[i]; i++ ) {
            value = Exhibit.getAttribute( node, attr );
            if( value ) {
                        result[attr] = value;
                        found = true;
            }
        }
        return found && result;
    }

    // FIXME: it's probably a better idea to ask database.js for these lists:
    var typelist = [ "uri", "label", "pluralLabel" ];
    var proplist = [ "uri", "valueType", // [text|number|date|boolean|item|url]
                     "label", "reverseLabel",
                     "pluralLabel", "reversePluralLabel",
                     "groupingLabel", "reverseGroupingLabel" ];
    var columnProps = [ "valueParser", "arity" ];

    var parsed = {}; // accumulator of all data we scrape up (for loadData)
    var type = Exhibit.getAttribute( table, 'type' );
    var types = type && readAttributes( table, typelist );
    if( types ) {
        parsed.types = {};
        parsed.types[type] = types;
    }

    var fields = [], props = {}, columnData = [], row, col;
    var tr, trs = table.getElementsByTagName("tr");
    var th, ths = trs[0].getElementsByTagName("th");
    for( col = 0; th = ths[col]; col++ ) {
        var field = textOf( th ).trim();
        var attr = readAttributes( th, proplist );
        var name = Exhibit.getAttribute( th, 'name' );
        if( name ) {
            attr = attr || {};
            attr.label = attr.label || field;
            field = name;
        }
        if( attr ) {
            props[field] = attr;
            parsed.properties = props;
        }
        fields.push( field );
        attr = readAttributes( th, columnProps ) || {};
        if( attr.valueParser && attr.valueParser in window ) {
            attr.valueParser = window[attr.valueParser];
        } else { // provide a default valueParser:
            if( attr.arity == "single" ) {
                attr.valueParser = function( text, node, rowNo, colNo ) {
                    return text.trim();
                };
            } else {
                attr.valueParser = function( text, node, rowNo, colNo ) {
                    if( text.indexOf(';') == -1 )
                        return text.trim();

                    var data = text.split(';');
                    for( var i = 0; i<data.length; i++ )
                        data[i] = data[i].trim();

                    return data;
                };
            }
        }
        columnData[col] = attr;
    }

    var img, imgs = table.getElementsByTagName("img");
    while( img = imgs[0] ) // replace any images with their respective URLs
        img.parentNode.replaceChild( document.createTextNode( img.src ), img );

    var items = [], td, raw;
    for( row = 1; tr = trs[row]; row++ ) {
        var item = {};
        var tds = tr.getElementsByTagName("td");
        for( col = 0; td = tds[col]; col++ ) {
            var raw = textOf( td );
            data = columnData[col].valueParser( raw, td, row, col );
            if( data == null || raw === "" ) {
                continue;
            }
            if( typeof data == 'object' && !(data instanceof Array) ) {
                for( var property in data ) {
                    item[property] = data[property];
                }
            } else {
                item[fields[col]] = data;

               //Check for link to extract as well. If td cell has an A tag in
               //it, take the link from that and store it also. If the textContent
               //is stored in the "name" field, for example, then the link
               // in the cell is stored in the "name-link" field. Only the first
               // url is stored.
                for (var i=0; i < td.childNodes.length; i++) {
                    if (td.childNodes[i].tagName === "A") {
               	        var thehref = td.childNodes[i].getAttribute("href");
               	        if (thehref) {
	       	            fieldname = fields[col] + "-link";
    	                    item[fieldname] = thehref;
    	                    break;
		        }
                    }
                }
            }
        }

        if( type )
            item.type = type;

        items.push( item );
        parsed.items = items;
    }

    database.loadData( parsed, Exhibit.Persistence.resolveURL(location.href) );
};
<commit_msg>Better way of extracting links. If column header has "ex:valueType=textwithlink" extracts text and one link separately, also sets appropriate valueType for each.<commit_after>/*==================================================
 *  Exhibit.HtmlTableImporter
 *==================================================
 */

Exhibit.HtmlTableImporter = {
};
Exhibit.importers["text/html"] = Exhibit.HtmlTableImporter;

Exhibit.HtmlTableImporter.load = function(link, database, cont) {
    var url = typeof link == "string" ? link : link.href;
    if (url.substr(0,1) == "#") {
        try {
            var id = /#(.*)/.exec(f)[1];
            var table = document.getElementById(id);
            table.style.display = "none"; // as we are replacing it with the exhibit UI

            Exhibit.HtmlTableImporter.loadTable(table, database);
        } catch (e) {
            SimileAjax.Debug.exception(e);
        } finally {
            if (cont) {
                cont();
            }
        }
    } else if (typeof link != "string") {
		var xpath = link.getAttribute('ex:xpath');
		var columns = (link.getAttribute('ex:columns')).split(',');
		var babelURL = "http://simile.mit.edu/babel/html-extractor?" + [
			"xpath=" + xpath,
			"url=" + encodeURIComponent(url)
		].join("&");
		var fConvert = function(string) {
			var div = document.createElement("div");
			div.innerHTML = string;
			var table = div.firstChild;

		    var th, ths = table.getElementsByTagName("th");
			for( col = 0; th = ths[col]; col++ ) {
				var label = columns[col];
				th.setAttribute('ex:name', label);
			}

			Exhibit.HtmlTableImporter.loadTable(table, database);
			return {};
		}
        return Exhibit.JSONPImporter.load(babelURL, database, cont, fConvert);
    } else {
        if (cont) {
            cont();
        }
    }
}

Exhibit.HtmlTableImporter.loadTable = function(table, database) {

    var textOf = function( n ) { return n.textContent || n.innerText || ""; };
    var readAttributes = function( node, attributes ) {
        var result = {}, found = false, attr, value, i;
        for( i = 0; attr = attributes[i]; i++ ) {
            value = Exhibit.getAttribute( node, attr );
            if( value ) {
                        result[attr] = value;
                        found = true;
            }
        }
        return found && result;
    }

    // FIXME: it's probably a better idea to ask database.js for these lists:
    var typelist = [ "uri", "label", "pluralLabel" ];
    var proplist = [ "uri", "valueType", // [text|number|date|boolean|item|url|textwithlink]
                     "label", "reverseLabel",
                     "pluralLabel", "reversePluralLabel",
                     "groupingLabel", "reverseGroupingLabel" ];
    var columnProps = [ "valueParser", "arity" ];

    var parsed = {}; // accumulator of all data we scrape up (for loadData)
    var type = Exhibit.getAttribute( table, 'type' );
    var types = type && readAttributes( table, typelist );
    if( types ) {
        parsed.types = {};
        parsed.types[type] = types;
    }

    var fields = [], props = {}, columnData = [], row, col;
    var tr, trs = table.getElementsByTagName("tr");
    var th, ths = trs[0].getElementsByTagName("th");
    for( col = 0; th = ths[col]; col++ ) {
        var field = textOf( th ).trim();
        var hastextwithlink = false;
        var attr = readAttributes( th, proplist );
        var name = Exhibit.getAttribute( th, 'name' );
        if( name ) {
            attr = attr || {};
            attr.label = attr.label || field;
            field = name;
        }
        if( attr ) {
            props[field] = attr;
            if (props[field].valueType == "textwithlink") {
            	props[field].valueType = "text";
            	props[(field + "-link")] = {valueType : "url"};
            	hastextwithlink = true;
            }
            parsed.properties = props;
        }
        fields.push( field );
        attr = readAttributes( th, columnProps ) || {};
        if( attr.valueParser && attr.valueParser in window ) {
            attr.valueParser = window[attr.valueParser];
        } else { // provide a default valueParser:
            if( attr.arity == "single" ) {
                attr.valueParser = function( text, node, rowNo, colNo ) {
                    return text.trim();
                };
            } else {
                attr.valueParser = function( text, node, rowNo, colNo ) {
                    if( text.indexOf(';') == -1 )
                        return text.trim();

                    var data = text.split(';');
                    for( var i = 0; i<data.length; i++ )
                        data[i] = data[i].trim();

                    return data;
                };
                if( hastextandlink) {
                    var fallback = attr.valueParser;
                    attr.valueParser = function( text, node, rowNo, colNo ) {
                        var links = node.getElementsByTagName("a");
                        if( !links.length ) { return fallback( text, node, rowNo, colNo ); }
                        var data={};
                        data[fields[colNo]] = text.trim();
                        data[(fields[colNo] + "-link") ] = links[0].href;
                        return data;
                    }
                }
            }
        }
        columnData[col] = attr;
    }

    var img, imgs = table.getElementsByTagName("img");
    while( img = imgs[0] ) // replace any images with their respective URLs
        img.parentNode.replaceChild( document.createTextNode( img.src ), img );

    var items = [], td, raw;
    for( row = 1; tr = trs[row]; row++ ) {
        var item = {};
        var tds = tr.getElementsByTagName("td");
        for( col = 0; td = tds[col]; col++ ) {
            var raw = textOf( td );
            data = columnData[col].valueParser( raw, td, row, col );
            if( data == null || raw === "" ) {
                continue;
            }
            if( typeof data == 'object' && !(data instanceof Array) ) {
                for( var property in data ) {
                    item[property] = data[property];
                }
            } else {
                item[fields[col]] = data;
            }
        }

        if( type )
            item.type = type;

        items.push( item );
        parsed.items = items;
    }

    database.loadData( parsed, Exhibit.Persistence.resolveURL(location.href) );
};
<|endoftext|>


<commit_before>angular.module('sar-database').controller("CourseRosterCtrl", ['$stateParams', '$scope', 'trainingService', function ($stateParams, $scope, Training) {
  angular.extend($scope, {
    query: {
      order: 'name',
      limit: 5,
      page: 1
    },
    showExpired: false,
    doFilter: function (item) {
      return $scope.showExpired || new Date(item.expires) >= new Date()
    },
    getRoster: function () {
      $scope.loading = Training.courses.one($stateParams.id).all('roster').getList().then(function (data) {
        $scope.roster = data;
      })
    },
    course: {},
    roster: []
  });
  $scope.getRoster()
  Training.courses.one($stateParams.id).get().then(function (course) {
    $scope.course = course;
  })
}]);<commit_msg>course roster - don't filter out non-expiring courses<commit_after>angular.module('sar-database').controller("CourseRosterCtrl", ['$stateParams', '$scope', 'trainingService', function ($stateParams, $scope, Training) {
  angular.extend($scope, {
    query: {
      order: 'name',
      limit: 5,
      page: 1
    },
    showExpired: false,
    doFilter: function (item) {
      return $scope.showExpired || (!item.expires || (new Date(item.expires) >= new Date()))
    },
    getRoster: function () {
      $scope.loading = Training.courses.one($stateParams.id).all('roster').getList().then(function (data) {
        $scope.roster = data;
      })
    },
    course: {},
    roster: []
  });
  $scope.getRoster()
  Training.courses.one($stateParams.id).get().then(function (course) {
    $scope.course = course;
  })
}]);<|endoftext|>


<commit_before>/*
 *     Copyright (c) 2011-2014 CoNWeT Lab., Universidad Politécnica de Madrid
 *
 *     This file is part of Wirecloud Platform.
 *
 *     Wirecloud Platform 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.
 *
 *     Wirecloud 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 Wirecloud Platform.  If not, see
 *     <http://www.gnu.org/licenses/>.
 *
 */

/*global LayoutManagerFactory, Wirecloud*/

(function () {

    "use strict";

    var onpopstate = function onpopstate(event) {
        if (event.state === null) {
            return;
        }

        LayoutManagerFactory.getInstance().onHashChange(event.state);
    };

    var prepareData = function prepareData(data) {
        var key, default_data = {
            view: "workspace"
        };
        data = Wirecloud.Utils.merge(default_data, data);
        for (key in data) {
            data[key] = "" + data[key];
        }
        return data;
    };

    var buildURL = function buildURL(data) {
        var key, hash = '';

        for (key in data) {
            if (key === 'workspace_name' || key === 'workspace_creator') {
                continue;
            }
            hash += '&' + encodeURI(key) + '=' + encodeURI(data[key]);
        }

        return window.location.protocol + "//" +
            window.location.host +
            "/" + encodeURIComponent(data.workspace_creator) + '/' + encodeURIComponent(data.workspace_name) +
            window.location.search +
            '#' + hash.substr(1);
    };

    var HistoryManager = {};

    HistoryManager.init = function init() {
        var initialState = this._parseStateFromHash(window.location.hash);
        this._parseWorkspaceFromPathName(window.location.pathname, initialState);

        window.addEventListener(
            "popstate",
            onpopstate.bind(this),
            true);

        initialState = prepareData(initialState);
        var url = buildURL(initialState);

        history.replaceState(initialState, document.title, url);
    };

    HistoryManager._parseStateFromHash = function _parseStateFromHash(hash) {
        var definitions, i, pair, key, value, data = {};

        if (hash.charAt(0) === '#') {
            hash = hash.substr(1);
        }
        if (hash.length === 0) {
            return data;
        }

        definitions = hash.split('&');
        for (i = 0; i < definitions.length; i += 1) {
            pair = definitions[i].split('=');
            key = decodeURIComponent(pair[0]);
            value = decodeURIComponent(pair[1]);
            data[key] = value;
        }

        return data;
    };

    HistoryManager._parseWorkspaceFromPathName = function _parseWorkspaceFromPathName(pathname, status) {
        var index, index2;

        index = pathname.lastIndexOf('/');
        index2 = pathname.lastIndexOf('/', index - 1);

        status.workspace_creator = decodeURIComponent(pathname.substring(index2 + 1, index));
        status.workspace_name = decodeURIComponent(pathname.substring(index + 1));
    };

    HistoryManager.pushState = function pushState(data) {
        var url, key, equal;

        data = prepareData(data);
        equal = true;
        for (key in data) {
            if (data[key] !== history.state[key]) {
                equal = false;
                break;
            }
        }
        if (equal) {
            return;
        }
        url = buildURL(data);

        history.pushState(data, "", url);
    };

    HistoryManager.replaceState = function replaceState(data) {
        var url, key, equal;

        data = prepareData(data);
        equal = true;
        for (key in data) {
            if (data[key] !== history.state[key]) {
                equal = false;
                break;
            }
        }
        if (equal) {
            return;
        }
        url = buildURL(data);

        history.replaceState(data, "", url);
    };

    HistoryManager.getCurrentState = function getCurrentState() {
        return Wirecloud.Utils.clone(history.state);
    };

    Wirecloud.HistoryManager = HistoryManager;

})();
<commit_msg>Work around browsers without the state property of the history object<commit_after>/*
 *     Copyright (c) 2011-2014 CoNWeT Lab., Universidad Politécnica de Madrid
 *
 *     This file is part of Wirecloud Platform.
 *
 *     Wirecloud Platform 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.
 *
 *     Wirecloud 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 Wirecloud Platform.  If not, see
 *     <http://www.gnu.org/licenses/>.
 *
 */

/*global LayoutManagerFactory, Wirecloud*/

(function () {

    "use strict";

    var currentState = {};

    var onpopstate = function onpopstate(event) {
        if (event.state === null) {
            return;
        }

        currentState = event.state;
        LayoutManagerFactory.getInstance().onHashChange(event.state);
    };

    var prepareData = function prepareData(data) {
        var key, default_data = {
            view: "workspace"
        };
        data = Wirecloud.Utils.merge(default_data, data);
        for (key in data) {
            data[key] = "" + data[key];
        }
        return data;
    };

    var buildURL = function buildURL(data) {
        var key, hash = '';

        for (key in data) {
            if (key === 'workspace_name' || key === 'workspace_creator') {
                continue;
            }
            hash += '&' + encodeURI(key) + '=' + encodeURI(data[key]);
        }

        return window.location.protocol + "//" +
            window.location.host +
            "/" + encodeURIComponent(data.workspace_creator) + '/' + encodeURIComponent(data.workspace_name) +
            window.location.search +
            '#' + hash.substr(1);
    };

    var HistoryManager = {};

    HistoryManager.init = function init() {
        var initialState = this._parseStateFromHash(window.location.hash);
        this._parseWorkspaceFromPathName(window.location.pathname, initialState);

        window.addEventListener(
            "popstate",
            onpopstate.bind(this),
            true);

        initialState = prepareData(initialState);
        var url = buildURL(initialState);

        history.replaceState(initialState, document.title, url);
        currentState = initialState;
    };

    HistoryManager._parseStateFromHash = function _parseStateFromHash(hash) {
        var definitions, i, pair, key, value, data = {};

        if (hash.charAt(0) === '#') {
            hash = hash.substr(1);
        }
        if (hash.length === 0) {
            return data;
        }

        definitions = hash.split('&');
        for (i = 0; i < definitions.length; i += 1) {
            pair = definitions[i].split('=');
            key = decodeURIComponent(pair[0]);
            value = decodeURIComponent(pair[1]);
            data[key] = value;
        }

        return data;
    };

    HistoryManager._parseWorkspaceFromPathName = function _parseWorkspaceFromPathName(pathname, status) {
        var index, index2;

        index = pathname.lastIndexOf('/');
        index2 = pathname.lastIndexOf('/', index - 1);

        status.workspace_creator = decodeURIComponent(pathname.substring(index2 + 1, index));
        status.workspace_name = decodeURIComponent(pathname.substring(index + 1));
    };

    HistoryManager.pushState = function pushState(data) {
        var url, key, equal;

        data = prepareData(data);
        equal = true;
        for (key in data) {
            if (data[key] !== currentState[key]) {
                equal = false;
                break;
            }
        }
        if (equal) {
            return;
        }
        url = buildURL(data);

        history.pushState(data, "", url);
        currentState = data;
    };

    HistoryManager.replaceState = function replaceState(data) {
        var url, key, equal;

        data = prepareData(data);
        equal = true;
        for (key in data) {
            if (data[key] !== currentState[key]) {
                equal = false;
                break;
            }
        }
        if (equal) {
            return;
        }
        url = buildURL(data);

        history.replaceState(data, "", url);
        currentState = data;
    };

    HistoryManager.getCurrentState = function getCurrentState() {
        return Wirecloud.Utils.clone(currentState);
    };

    Wirecloud.HistoryManager = HistoryManager;

})();
<|endoftext|>


<commit_before>
Tinytest.add("stylus - presence", function(test) {

  var div = document.createElement('div');
  Blaze.render(Template.stylus_test_presence).attach(div);
  div.style.display = 'block';
  document.body.appendChild(div);

  var p = div.querySelector('p');
  var leftBorder = getStyleProperty(p, 'border-left-style');
  test.equal(leftBorder, "dashed");

  document.body.removeChild(div);
});

Tinytest.add("stylus - @import", function(test) {
  var div = document.createElement('div');
  Blaze.render(Template.stylus_test_import).attach(div);
  div.style.display = 'block';
  document.body.appendChild(div);

  var p = div.querySelector('p');
  test.equal(getStyleProperty(p, 'font-size'), "20px");
  test.equal(getStyleProperty(p, 'border-left-style'), "dashed");

  document.body.removeChild(div);
});
<commit_msg>UI.render = Blaze.render, returns a View<commit_after>
Tinytest.add("stylus - presence", function(test) {

  var div = document.createElement('div');
  UI.insert(UI.render(Template.stylus_test_presence), div);
  div.style.display = 'block';
  document.body.appendChild(div);

  var p = div.querySelector('p');
  var leftBorder = getStyleProperty(p, 'border-left-style');
  test.equal(leftBorder, "dashed");

  document.body.removeChild(div);
});

Tinytest.add("stylus - @import", function(test) {
  var div = document.createElement('div');
  UI.insert(UI.render(Template.stylus_test_import), div);
  div.style.display = 'block';
  document.body.appendChild(div);

  var p = div.querySelector('p');
  test.equal(getStyleProperty(p, 'font-size'), "20px");
  test.equal(getStyleProperty(p, 'border-left-style'), "dashed");

  document.body.removeChild(div);
});
<|endoftext|>


<commit_before>var addOptionTextRow = {};
var removeOptionTextRow = {};
$(function() {
    var updateName = function(name, index) {
        var sub = name.substring(0, name.length - 1);
        return sub + index;
    };
    var clearContent = function(row) {
        for (var index = 0; index < row.childNodes.length; index++) {
            var cell = row.childNodes[index];
            var element = cell.childNodes[0];
            var name = $(element).attr('name');
            if (name) {
                $(cell.childNodes[0]).val('');
            }
        }
    };
    var generateId = function(root, row) {
        var rowCount = $(root).data('rowCount');
        if (!rowCount) {
            rowCount = 1;
            $(root).data('rowCount', rowCount);
        } else {
            rowCount++;
            $(root).data('rowCount', rowCount);
        }
        for (var index = 0; index < row.childNodes.length; index++) {
            var cell = row.childNodes[index];
            var element = cell.childNodes[0];
            var name = $(element).attr('name');
            if (name) {
                $(element).attr('name', updateName(name, rowCount));
            }
        }
    };
    var countRows = function(root) {
        var count = root.childNodes.length;
        return count;
    };
    addOptionTextRow = function(el) {
        var root = el.parentNode.parentNode.parentNode;
        //Check if there is a saved template in the root
        var template = $(root).data('template');
        if (template) {
            generateId(root, template);
            var clone = $(template).clone();
        } else {
            var row = root.childNodes[2];
            generateId(root, row);
            var clone = $(row).clone();
        }
        clearContent(clone[0]);
        clone.appendTo(root);
    };
    removeOptionTextRow = function(el) {
        var row = el.parentNode.parentNode;
        var root = row.parentNode;
        var rowCount = $(root).data('rowCount');
        if (!rowCount) {
            rowCount = countRows(root);
            $(root).data('rowCount', rowCount);
        }
        $(root).data('template', row);
        //Cache this row before removal
        $(row).remove();
    };
});<commit_msg>Refactored the option text code to remove duplication<commit_after>/**
 * The script is used to handle the rendering of the options text field type.It handles
 * - Adding of new rows
 * - Removal of existing rows;
 * @type {Object}
 */
var addOptionTextRow = {};
var removeOptionTextRow = {};
$(function() {
    var ROW_COUNT = 'rowCount'; //A counter which maintains the number of rows created in order to generate unique ids for each row
    var ROW_TEMPLATE = 'template';
    /**
     * The function generates a unique name given the current name of an element and an index
     * @param  {[type]} name  The current name of the element
     * @param  {[type]} index A unique index value
     * @return {[type]}       A unique name based on the current name and the provided index.It can be considered unique for the lifetime of the page
     */
    var updateName = function(name, index) {
        var sub = name.substring(0, name.length - 1);
        return sub + index;
    };
    /**
     * The function resets the contents of the row since the new rows
     * which are added contain the values of the template
     * @param  {[type]} row [description]
     */
    var clearContent = function(row) {
        iterateRow(row, function(cell) {
            $(cell.childNodes[0]).val('');
        })
    };
    var iterateRow = function(row, fn) {
        for (var index = 0; index < row.childNodes.length; index++) {
            var cell = row.childNodes[index];
            var element = cell.childNodes[0];
            var name = $(element).attr('name');
            if (name) {
                fn(cell);
            }
        }
    };
    var generateId = function(root, row) {
        var rowCount = $(root).data(ROW_COUNT);
        if (!rowCount) {
            rowCount = 1;
        } else {
            rowCount++;
        }
        $(root).data(ROW_COUNT, rowCount);
        iterateRow(row, function(cell) {
            var element = cell.childNodes[0];
            var name = $(element).attr('name');
            $(element).attr('name', updateName(name, rowCount));
        });
    };
    var countRows = function(root) {
        var count = root.childNodes.length;
        return count;
    };
    addOptionTextRow = function(el) {
        var root = el.parentNode.parentNode.parentNode;
        //Check if there is a saved template in the root
        var template = $(root).data(ROW_TEMPLATE);
        if (template) {
            generateId(root, template);
            var clone = $(template).clone();
        } else {
            var row = root.childNodes[2];
            generateId(root, row);
            var clone = $(row).clone();
        }
        clearContent(clone[0]);
        clone.appendTo(root);
    };
    removeOptionTextRow = function(el) {
        var row = el.parentNode.parentNode;
        var root = row.parentNode;
        var rowCount = $(root).data(ROW_COUNT);
        if (!rowCount) {
            rowCount = countRows(root);
            $(root).data(ROW_COUNT, rowCount);
        }
        //Cache this row before removal
        $(root).data(ROW_TEMPLATE, row);
        $(row).remove();
    };
});<|endoftext|>


<commit_before>/**
* PropertyFinder Search.
*/
'use strict';
import React, {
  StyleSheet,
  Text,
  TextInput,
  View,
  TouchableHighlight,
  ActivityIndicatorIOS,
  Image,
  Component,
} from 'react-native';

/**
*
*/
function urlForQueryAndPage(key, value, pageNumber) {
  var data = {
      country: 'uk',
      pretty: '1',
      encoding: 'json',
      listing_type: 'buy',
      action: 'search_listings',
      page: pageNumber
  };
  data[key] = value;

  var querystring = Object.keys(data)
    .map(key => key + '=' + encodeURIComponent(data[key]))
    .join('&');

  return 'http://api.nestoria.co.uk/api?' + querystring;
};

/**
*
*/
class SearchPage extends Component {
  constructor(props) {
      super(props);
      this.state = {
        searchString: 'london',
      };
  }

  onSearchTextChanges(event) {
    this.setState({
      searchString: event.nativeEvent.text,
      isLoading: false,
      message: '',
    });
  }

  _executeQuery(query) {
    console.log(query);
    this.setState({ isLoading: true });

    fetch(query)
      .then(response => response.json())
      .then(json => this._handleResponse(json.response))
      .catch(error =>
        this.setState({
          loading: false,
          message: 'Something bad happened ' + error
        }));
  }

  _handleResponse(response) {
    this.setState({
      isLoading: false,
      message: ''
    });

    if( response.application_response_code.substr(0, 1)=== '1' ){
      this.setState({
        message: 'Properties found: ' + response.listings.length,
      });
    }
    else {
      this.setState({
        message: 'Location not recognized; please try again',
      });
    }
  }

  onSearchPressed(event) {
    var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
    this._executeQuery(query);
  }

  render() {
    var spinner = this.state.isLoading ? (<ActivityIndicatorIOS hidden='true' size='large'/>) : (<View/>);

    return(
      <View style={styles.container}>

        <Text style={styles.description}>
          Search for houses to buy
        </Text>
        <Text style={styles.description}>
            Search by place-name, postcode or search near your location.
        </Text>

        <View style={styles.flowRight}>
          <TextInput
            style={styles.searchInput}
            value={this.state.searchString}
            onChange={this.onSearchTextChanges.bind(this)}
            placeholder='Search via name or postcode'/>
          <TouchableHighlight style={styles.button}
            underlayColor='#99d9f4'
            onPress={this.onSearchPressed.bind(this)}>
            <Text style={styles.buttonText}>Go</Text>
          </TouchableHighlight>
        </View>

        <TouchableHighlight style={styles.button}
            underlayColor='#99d9f4'>
          <Text style={styles.buttonText}>Location</Text>
        </TouchableHighlight>
        <Image source={require('image!house')} style={styles.image}/>
        {spinner}

        <Text style={styles.message}>{this.state.message}</Text>

      </View>
    )
  }
}

var styles = StyleSheet.create({
  description: {
    marginBottom: 20,
    fontSize: 18,
    textAlign: 'center',
    color: '#656565'
  },
  container: {
    padding: 30,
    marginTop: 65,
    alignItems: 'center'
  },
  image: {
    width: 217,
    height: 138
  },
  flowRight: {
    flexDirection: 'row',
    alignItems: 'center',
    alignSelf: 'stretch'
  },
  buttonText: {
    fontSize: 18,
    color: 'white',
    alignSelf: 'center'
  },
  button: {
    height: 36,
    flex: 1,
    flexDirection: 'row',
    backgroundColor: '#48BBEC',
    borderColor: '#48BBEC',
    borderWidth: 1,
    borderRadius: 8,
    marginBottom: 10,
    alignSelf: 'stretch',
    justifyContent: 'center'
  },
  searchInput: {
    height: 36,
    padding: 4,
    marginRight: 5,
    flex: 4,
    fontSize: 18,
    borderWidth: 1,
    borderColor: '#48BBEC',
    borderRadius: 8,
    color: '#48BBEC'
  }
});

module.exports = SearchPage;
<commit_msg>redirect for results<commit_after>/**
* PropertyFinder Search.
*/
'use strict';
import React, {
  StyleSheet,
  Text,
  TextInput,
  View,
  TouchableHighlight,
  ActivityIndicatorIOS,
  Image,
  Component,
} from 'react-native';

var SearchResults = require('./searchResults.js');

/**
*
*/
function urlForQueryAndPage(key, value, pageNumber) {
  var data = {
      country: 'uk',
      pretty: '1',
      encoding: 'json',
      listing_type: 'buy',
      action: 'search_listings',
      page: pageNumber
  };
  data[key] = value;

  var querystring = Object.keys(data)
    .map(key => key + '=' + encodeURIComponent(data[key]))
    .join('&');

  return 'http://api.nestoria.co.uk/api?' + querystring;
};

/**
*
*/
class SearchPage extends Component {
  constructor(props) {
      super(props);
      this.state = {
        searchString: 'london',
      };
  }

  onSearchTextChanges(event) {
    this.setState({
      searchString: event.nativeEvent.text,
      isLoading: false,
      message: '',
    });
  }

  _executeQuery(query) {
    console.log(query);
    this.setState({ isLoading: true });

    fetch(query)
      .then(response => response.json())
      .then(json => this._handleResponse(json.response))
      .catch(error =>
        this.setState({
          loading: false,
          message: 'Something bad happened ' + error
        }));
  }

  _handleResponse(response) {
    this.setState({
      isLoading: false,
      message: ''
    });

    if( response.application_response_code.substr(0, 1)=== '1' ){
      console.log(response.listings.length);
      this.props.navigator.push({
        title: 'Results',
        component: SearchResults,
        passProps: {listings: response.listings}
      });
    }
    else {
      this.setState({
        message: 'Location not recognized; please try again',
      });
    }
  }

  onSearchPressed(event) {
    var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
    this._executeQuery(query);
  }

  render() {
    var spinner = this.state.isLoading ? (<ActivityIndicatorIOS hidden='true' size='large'/>) : (<View/>);

    return(
      <View style={styles.container}>

        <Text style={styles.description}>
          Search for houses to buy
        </Text>
        <Text style={styles.description}>
            Search by place-name, postcode or search near your location.
        </Text>

        <View style={styles.flowRight}>
          <TextInput
            style={styles.searchInput}
            value={this.state.searchString}
            onChange={this.onSearchTextChanges.bind(this)}
            placeholder='Search via name or postcode'/>
          <TouchableHighlight style={styles.button}
            underlayColor='#99d9f4'
            onPress={this.onSearchPressed.bind(this)}>
            <Text style={styles.buttonText}>Go</Text>
          </TouchableHighlight>
        </View>

        <TouchableHighlight style={styles.button}
            underlayColor='#99d9f4'>
          <Text style={styles.buttonText}>Location</Text>
        </TouchableHighlight>
        <Image source={require('image!house')} style={styles.image}/>
        {spinner}

        <Text style={styles.message}>{this.state.message}</Text>

      </View>
    )
  }
}

var styles = StyleSheet.create({
  description: {
    marginBottom: 20,
    fontSize: 18,
    textAlign: 'center',
    color: '#656565'
  },
  container: {
    padding: 30,
    marginTop: 65,
    alignItems: 'center'
  },
  image: {
    width: 217,
    height: 138
  },
  flowRight: {
    flexDirection: 'row',
    alignItems: 'center',
    alignSelf: 'stretch'
  },
  buttonText: {
    fontSize: 18,
    color: 'white',
    alignSelf: 'center'
  },
  button: {
    height: 36,
    flex: 1,
    flexDirection: 'row',
    backgroundColor: '#48BBEC',
    borderColor: '#48BBEC',
    borderWidth: 1,
    borderRadius: 8,
    marginBottom: 10,
    alignSelf: 'stretch',
    justifyContent: 'center'
  },
  searchInput: {
    height: 36,
    padding: 4,
    marginRight: 5,
    flex: 4,
    fontSize: 18,
    borderWidth: 1,
    borderColor: '#48BBEC',
    borderRadius: 8,
    color: '#48BBEC'
  }
});

module.exports = SearchPage;
<|endoftext|>


<commit_before>//----------------------------------------------------------------------------
//  Copyright (C) 2008-2011  The IPython Development Team
//
//  Distributed under the terms of the BSD License.  The full license is in
//  the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------

//============================================================================
// SaveWidget
//============================================================================

var IPython = (function (IPython) {

    var utils = IPython.utils;

    var SaveWidget = function (selector) {
        this.selector = selector;
        if (this.selector !== undefined) {
            this.element = $(selector);
            this.style();
            this.bind_events();
        }
    };

    SaveWidget.prototype.style = function () {
        this.element.find('span#save_widget').addClass('ui-widget');
        this.element.find('span#notebook_name').addClass('ui-widget ui-widget-content');
        this.element.find('span#save_status').addClass('ui-widget ui-widget-content')
            .css({border: 'none', 'margin-left': '20px'});
    };

    SaveWidget.prototype.bind_events = function () {
        var that = this;
        this.element.find('span#notebook_name').click(function () {
            that.rename_notebook();
        });
        this.element.find('span#notebook_name').hover(function () {
            $(this).addClass("ui-state-hover");
        }, function () {
            $(this).removeClass("ui-state-hover");
        });
        $([IPython.events]).on('notebook_loaded.Notebook', function () {
            that.set_last_saved();
            that.update_notebook_name();
            that.update_document_title();
        });
        $([IPython.events]).on('notebook_saved.Notebook', function () {
            that.set_last_saved();
            that.update_notebook_name();
            that.update_document_title();
        });
        $([IPython.events]).on('notebook_save_failed.Notebook', function () {
            that.set_save_status('');
        });
    };

    SaveWidget.prototype.rename_notebook = function () {
        var that = this;
        var dialog = $('<div/>');
        dialog.append(
            $('<h3/>').html('Enter a new notebook name:')
            .css({'margin-bottom': '10px'})
        );
        dialog.append(
            $('<input/>').attr('type','text').attr('size','25')
            .addClass('ui-widget ui-widget-content')
            .attr('value',IPython.notebook.get_notebook_name())
        );
        // $(document).append(dialog);
        dialog.dialog({
            resizable: false,
            modal: true,
            title: "Rename Notebook",
            closeText: "",
            close: function(event, ui) {$(this).dialog('destroy').remove();},
            buttons : {
                "OK": function () {
                    var new_name = $(this).find('input').attr('value');
                    if (!IPython.notebook.test_notebook_name(new_name)) {
                        $(this).find('h3').html(
                            "Invalid notebook name. Notebook names must "+
                            "have 1 or more characters and can contain any characters " +
                            "except / and \\. Please enter a new notebook name:"
                        );
                    } else {
                        IPython.notebook.set_notebook_name(new_name);
                        IPython.notebook.save_notebook();
                        $(this).dialog('close');
                    }
                },
                "Cancel": function () {
                    $(this).dialog('close');
                }
            }
        });
    }

    SaveWidget.prototype.update_notebook_name = function () {
        var nbname = IPython.notebook.get_notebook_name();
        this.element.find('span#notebook_name').html(nbname);
    };

    SaveWidget.prototype.update_document_title = function () {
        var nbname = IPython.notebook.get_notebook_name();
        document.title = nbname;
    };

    SaveWidget.prototype.update_url = function () {
        var notebook_id = IPython.notebook.get_notebook_id();
        if (notebook_id !== null) {
            var new_url = $('body').data('baseProjectUrl') + notebook_id;
            window.history.replaceState({}, '', new_url);
        };
    };

    SaveWidget.prototype.set_save_status = function (msg) {
        this.element.find('span#save_status').html(msg);
    }

    SaveWidget.prototype.set_last_saved = function () {
        var d = new Date();
        this.set_save_status('Last saved: '+d.format('mmm dd h:MM TT'));
    };

    IPython.SaveWidget = SaveWidget;

    return IPython;

}(IPython));

<commit_msg>ENTER submits the rename notebook dialog.<commit_after>//----------------------------------------------------------------------------
//  Copyright (C) 2008-2011  The IPython Development Team
//
//  Distributed under the terms of the BSD License.  The full license is in
//  the file COPYING, distributed as part of this software.
//----------------------------------------------------------------------------

//============================================================================
// SaveWidget
//============================================================================

var IPython = (function (IPython) {

    var utils = IPython.utils;

    var SaveWidget = function (selector) {
        this.selector = selector;
        if (this.selector !== undefined) {
            this.element = $(selector);
            this.style();
            this.bind_events();
        }
    };

    SaveWidget.prototype.style = function () {
        this.element.find('span#save_widget').addClass('ui-widget');
        this.element.find('span#notebook_name').addClass('ui-widget ui-widget-content');
        this.element.find('span#save_status').addClass('ui-widget ui-widget-content')
            .css({border: 'none', 'margin-left': '20px'});
    };

    SaveWidget.prototype.bind_events = function () {
        var that = this;
        this.element.find('span#notebook_name').click(function () {
            that.rename_notebook();
        });
        this.element.find('span#notebook_name').hover(function () {
            $(this).addClass("ui-state-hover");
        }, function () {
            $(this).removeClass("ui-state-hover");
        });
        $([IPython.events]).on('notebook_loaded.Notebook', function () {
            that.set_last_saved();
            that.update_notebook_name();
            that.update_document_title();
        });
        $([IPython.events]).on('notebook_saved.Notebook', function () {
            that.set_last_saved();
            that.update_notebook_name();
            that.update_document_title();
        });
        $([IPython.events]).on('notebook_save_failed.Notebook', function () {
            that.set_save_status('');
        });
    };

    SaveWidget.prototype.rename_notebook = function () {
        var that = this;
        var dialog = $('<div/>');
        dialog.append(
            $('<h3/>').html('Enter a new notebook name:')
            .css({'margin-bottom': '10px'})
        );
        dialog.append(
            $('<input/>').attr('type','text').attr('size','25')
            .addClass('ui-widget ui-widget-content')
            .attr('value',IPython.notebook.get_notebook_name())
        );
        // $(document).append(dialog);
        dialog.dialog({
            resizable: false,
            modal: true,
            title: "Rename Notebook",
            closeText: "",
            close: function(event, ui) {$(this).dialog('destroy').remove();},
            buttons : {
                "OK": function () {
                    var new_name = $(this).find('input').attr('value');
                    if (!IPython.notebook.test_notebook_name(new_name)) {
                        $(this).find('h3').html(
                            "Invalid notebook name. Notebook names must "+
                            "have 1 or more characters and can contain any characters " +
                            "except / and \\. Please enter a new notebook name:"
                        );
                    } else {
                        IPython.notebook.set_notebook_name(new_name);
                        IPython.notebook.save_notebook();
                        $(this).dialog('close');
                    }
                },
                "Cancel": function () {
                    $(this).dialog('close');
                }
            },
            open : function (event, ui) {
                var that = $(this);
                // Upon ENTER, click the OK button.
                that.keydown(function (event, ui) {
                    if (event.which === 13) {
                        that.parent().find('button').first().click();
                    }
                });
            }
        });
    }

    SaveWidget.prototype.update_notebook_name = function () {
        var nbname = IPython.notebook.get_notebook_name();
        this.element.find('span#notebook_name').html(nbname);
    };

    SaveWidget.prototype.update_document_title = function () {
        var nbname = IPython.notebook.get_notebook_name();
        document.title = nbname;
    };

    SaveWidget.prototype.update_url = function () {
        var notebook_id = IPython.notebook.get_notebook_id();
        if (notebook_id !== null) {
            var new_url = $('body').data('baseProjectUrl') + notebook_id;
            window.history.replaceState({}, '', new_url);
        };
    };

    SaveWidget.prototype.set_save_status = function (msg) {
        this.element.find('span#save_status').html(msg);
    }

    SaveWidget.prototype.set_last_saved = function () {
        var d = new Date();
        this.set_save_status('Last saved: '+d.format('mmm dd h:MM TT'));
    };

    IPython.SaveWidget = SaveWidget;

    return IPython;

}(IPython));

<|endoftext|>


<commit_before>import Users from 'meteor/vulcan:users';

const generateIdResolverSingle = ({collectionName, fieldName}) => {
  return async (doc, args, context) => {
    if (!doc[fieldName]) return null

    const { currentUser } = context
    const collection = context[collectionName]
    const { checkAccess } = collection

    const resolvedDoc = await collection.loader.load(doc[fieldName])
    if (checkAccess && !checkAccess(currentUser, resolvedDoc)) return null
    const restrictedDoc = Users.restrictViewableFields(currentUser, collection, resolvedDoc)

    return restrictedDoc
  }
}

const generateIdResolverMulti = ({collectionName, fieldName}) => {
  return async (doc, args, context) => {
    if (!doc[fieldName]) return []

    const { currentUser } = context
    const collection = context[collectionName]
    const { checkAccess } = collection

    const resolvedDocs = await collection.loader.loadMany(doc[fieldName])
    const existingDocs = _.filter(resolvedDocs, d=>!!d);
    const filteredDocs = checkAccess ? _.filter(existingDocs, d => checkAccess(currentUser, d)) : resolvedDocs
    const restrictedDocs = Users.restrictViewableFields(currentUser, collection, filteredDocs)

    return restrictedDocs
  }
}

export const foreignKeyField = ({idFieldName, resolverName, collectionName, type}) => {
  if (!idFieldName || !resolverName || !collectionName || !type)
    throw new Error("Missing argument to foreignKeyField");

  return {
    type: String,
    foreignKey: collectionName,
    resolveAs: {
      fieldName: resolverName,
      type: type,
      resolver: generateIdResolverSingle({
        collectionName,
        fieldName: idFieldName
      }),
      addOrginalField: true,
    },
  }
}

export const arrayOfForeignKeysField = ({idFieldName, resolverName, collectionName, type}) => {
  return {
    type: Array,
    resolveAs: {
      fieldName: resolverName,
      type: `[${type}]`,
      resolver: generateIdResolverMulti({
        collectionName,
        fieldName: idFieldName
      }),
      addOriginalField: true
    },
  }
}

const simplSchemaToGraphQLtype = (type) => {
  if (type === String) return "String";
  else if (type === Number) return "Int";
  else if (type === Date) return "Date";
}

export const resolverOnlyField = ({type, graphQLtype=null, resolver, ...rest}) => {
  return {
    type: type,
    optional: true,
    resolveAs: {
      type: graphQLtype || simplSchemaToGraphQLtype(type),
      resolver: resolver,
    },
    ...rest
  }
}

// Given a collection and a fieldName=>fieldSchema dictionary, add fields to
// the collection schema. We use this instead of collection.addField([...])
// because that one forces an awkward syntax in order to be array-based instead
// of object-based.
export const addFieldsDict = (collection, fieldsDict) => {
  let translatedFields = [];
  for (let key in fieldsDict) {
    translatedFields.push({
      fieldName: key,
      fieldSchema: fieldsDict[key]
    });
  }
  collection.addField(translatedFields);
}<commit_msg>Fix an embarrassing typo that broke tests<commit_after>import Users from 'meteor/vulcan:users';

const generateIdResolverSingle = ({collectionName, fieldName}) => {
  return async (doc, args, context) => {
    if (!doc[fieldName]) return null

    const { currentUser } = context
    const collection = context[collectionName]
    const { checkAccess } = collection

    const resolvedDoc = await collection.loader.load(doc[fieldName])
    if (checkAccess && !checkAccess(currentUser, resolvedDoc)) return null
    const restrictedDoc = Users.restrictViewableFields(currentUser, collection, resolvedDoc)

    return restrictedDoc
  }
}

const generateIdResolverMulti = ({collectionName, fieldName}) => {
  return async (doc, args, context) => {
    if (!doc[fieldName]) return []

    const { currentUser } = context
    const collection = context[collectionName]
    const { checkAccess } = collection

    const resolvedDocs = await collection.loader.loadMany(doc[fieldName])
    const existingDocs = _.filter(resolvedDocs, d=>!!d);
    const filteredDocs = checkAccess ? _.filter(existingDocs, d => checkAccess(currentUser, d)) : resolvedDocs
    const restrictedDocs = Users.restrictViewableFields(currentUser, collection, filteredDocs)

    return restrictedDocs
  }
}

export const foreignKeyField = ({idFieldName, resolverName, collectionName, type}) => {
  if (!idFieldName || !resolverName || !collectionName || !type)
    throw new Error("Missing argument to foreignKeyField");

  return {
    type: String,
    foreignKey: collectionName,
    resolveAs: {
      fieldName: resolverName,
      type: type,
      resolver: generateIdResolverSingle({
        collectionName,
        fieldName: idFieldName
      }),
      addOriginalField: true,
    },
  }
}

export const arrayOfForeignKeysField = ({idFieldName, resolverName, collectionName, type}) => {
  return {
    type: Array,
    resolveAs: {
      fieldName: resolverName,
      type: `[${type}]`,
      resolver: generateIdResolverMulti({
        collectionName,
        fieldName: idFieldName
      }),
      addOriginalField: true
    },
  }
}

const simplSchemaToGraphQLtype = (type) => {
  if (type === String) return "String";
  else if (type === Number) return "Int";
  else if (type === Date) return "Date";
}

export const resolverOnlyField = ({type, graphQLtype=null, resolver, ...rest}) => {
  return {
    type: type,
    optional: true,
    resolveAs: {
      type: graphQLtype || simplSchemaToGraphQLtype(type),
      resolver: resolver,
    },
    ...rest
  }
}

// Given a collection and a fieldName=>fieldSchema dictionary, add fields to
// the collection schema. We use this instead of collection.addField([...])
// because that one forces an awkward syntax in order to be array-based instead
// of object-based.
export const addFieldsDict = (collection, fieldsDict) => {
  let translatedFields = [];
  for (let key in fieldsDict) {
    translatedFields.push({
      fieldName: key,
      fieldSchema: fieldsDict[key]
    });
  }
  collection.addField(translatedFields);
}<|endoftext|>