<commit_before>/**
 * Module dependencies.
 */
var os = require('os');
var get = require('lodash.get');

/**
 * Main function that converts json to csv
 *
 * @param {Object} params Function parameters containing data, fields,
 * delimiter (default is ','), hasCSVColumnTitle (default is true)
 * and default value (default is '')
 * @param {Function} callback(err, csv) - Callback function
 *   if error, returning error in call back.
 *   if csv is created successfully, returning csv output to callback.
 */
module.exports = function (params, callback) {
  if (!callback || typeof callback !== 'function') {
    throw new Error('Callback is required');
  }

  checkParams(params, function (err) {
    if (err) {
      return callback(err);
    }

    var titles = createColumnTitles(params);
    var csv = createColumnContent(params, titles);

    callback(null, csv);
  });
};

/**
 * Check passing params
 *
 * @param {Object} params Function parameters containing data, fields,
 * delimiter, default value, mark quotes and hasCSVColumnTitle
 * @param {Function} callback Callback function returning error when invalid field is found
 */
function checkParams(params, callback) {
  // if data is an Object, not in array [{}], then just create 1 item array.
  // So from now all data in array of object format.
  if (!Array.isArray(params.data)) {
    var ar = [];
    ar[0] = params.data;
    params.data = ar;
  }

  if (!params.fields && params.data && params.data.length) {
    if(typeof params.data[0] === 'object' && params.data[0] !== null){
      params.fields = Object.keys(params.data[0]);
    }else{
      return callback(new Error('params should be a valid object.'));
    }
  }

  //#check fieldNames
  if (params.fieldNames && params.fieldNames.length !== params.fields.length) {
    return callback(new Error('fieldNames and fields should be of the same length, if fieldNames is provided.'));
  }

  params.fieldNames = params.fieldNames || params.fields;

  //#check delimiter
  params.del = params.del || ',';

  //#check end of line character
  params.eol = params.eol || '';

  //#check quotation mark
  params.quotes = typeof params.quotes === 'string' ? params.quotes : '"';

  //#check nested option
  params.nested = params.nested || false;

  //#check default value
  params.defaultValue = params.defaultValue;

  //#check hasCSVColumnTitle, if it is not explicitly set to false then true.
  if (params.hasCSVColumnTitle !== false) {
    params.hasCSVColumnTitle = true;
  }

  callback(null);
}

/**
 * Create the title row with all the provided fields as column headings
 *
 * @param {Object} params Function parameters containing data, fields and delimiter
 * @returns {String} titles as a string
 */
function createColumnTitles(params) {
  var str = '';

  //if CSV has column title, then create it
  if (params.hasCSVColumnTitle) {
    params.fieldNames.forEach(function (element) {
      if (str !== '') {
        str += params.del;
      }
      str += JSON.stringify(element).replace(/\"/g, params.quotes);
    });
  }

  return str;
}

/**
 * Replace the quotation marks of the field element if needed (can be a not string-like item)
 *
 * @param {string} stringifiedElement The field element after JSON.stringify()
 * @param {string} quotes The params.quotes value. At this point we know that is not equal to double (")
 */
function replaceQuotationMarks(stringifiedElement, quotes) {
  var lastCharIndex = stringifiedElement.length - 1;

  //check if it's an string-like element
  if (stringifiedElement[0] === '"' && stringifiedElement[lastCharIndex] === '"') {
    //split the stringified field element because Strings are immutable
    var splitElement = stringifiedElement.split('');

    //replace the quotation marks
    splitElement[0] = quotes;
    splitElement[lastCharIndex] = quotes;

    //join again
    stringifiedElement = splitElement.join('');
  }

  return stringifiedElement;
}

/**
 * Create the content column by column and row by row below the title
 *
 * @param {Object} params Function parameters containing data, fields and delimiter
 * @param {String} str Title row as a string
 * @returns {String} csv string
 */
function createColumnContent(params, str) {
  params.data.forEach(function (dataElement) {
    //if null or empty object do nothing
    if (dataElement && Object.getOwnPropertyNames(dataElement).length > 0) {
      var line = '';
      var eol = params.newLine || os.EOL || '\n';

      params.fields.forEach(function (fieldElement) {
        var val;

        if (params.nested) {
          val = get(dataElement, fieldElement, '');
        } else {
          val = dataElement[fieldElement];
        }

        if (val !== undefined) {
          var stringifiedElement = JSON.stringify(val);

          if (params.quotes !== '"') {
            stringifiedElement = replaceQuotationMarks(stringifiedElement, params.quotes);
          }

          line += stringifiedElement;
        } else if (params.defaultValue !== undefined) {
          // Use default value if defined
          line += JSON.stringify(params.defaultValue);
        }

        line += params.del;
      });

      //remove last delimeter
      line = line.substring(0, line.length - 1);
      line = line.replace(/\\"/g, Array(3).join(params.quotes));
      //If header exists, add it, otherwise, print only content
      if (str !== '') {
        str += eol + line + params.eol;
      } else {
        str = line + params.eol;
      }
    }
  });

  return str;
}
<commit_msg>Fixup syntax from validation pr.<commit_after>/**
 * Module dependencies.
 */
var os = require('os');
var get = require('lodash.get');

/**
 * Main function that converts json to csv
 *
 * @param {Object} params Function parameters containing data, fields,
 * delimiter (default is ','), hasCSVColumnTitle (default is true)
 * and default value (default is '')
 * @param {Function} callback(err, csv) - Callback function
 *   if error, returning error in call back.
 *   if csv is created successfully, returning csv output to callback.
 */
module.exports = function (params, callback) {
  if (!callback || typeof callback !== 'function') {
    throw new Error('Callback is required');
  }

  checkParams(params, function (err) {
    if (err) {
      return callback(err);
    }

    var titles = createColumnTitles(params);
    var csv = createColumnContent(params, titles);

    callback(null, csv);
  });
};

/**
 * Check passing params
 *
 * @param {Object} params Function parameters containing data, fields,
 * delimiter, default value, mark quotes and hasCSVColumnTitle
 * @param {Function} callback Callback function returning error when invalid field is found
 */
function checkParams(params, callback) {
  // if data is an Object, not in array [{}], then just create 1 item array.
  // So from now all data in array of object format.
  if (!Array.isArray(params.data)) {
    var ar = [];
    ar[0] = params.data;
    params.data = ar;
  }

  if (!params.fields && params.data && params.data.length) {
    var firstData = params.data[0];

    if (typeof firstData === 'object' && firstData !== null) {
      params.fields = Object.keys(firstData);
    } else {
      return callback(new Error('params should be a valid object.'));
    }
  }

  //#check fieldNames
  if (params.fieldNames && params.fieldNames.length !== params.fields.length) {
    return callback(new Error('fieldNames and fields should be of the same length, if fieldNames is provided.'));
  }

  params.fieldNames = params.fieldNames || params.fields;

  //#check delimiter
  params.del = params.del || ',';

  //#check end of line character
  params.eol = params.eol || '';

  //#check quotation mark
  params.quotes = typeof params.quotes === 'string' ? params.quotes : '"';

  //#check nested option
  params.nested = params.nested || false;

  //#check default value
  params.defaultValue = params.defaultValue;

  //#check hasCSVColumnTitle, if it is not explicitly set to false then true.
  if (params.hasCSVColumnTitle !== false) {
    params.hasCSVColumnTitle = true;
  }

  callback(null);
}

/**
 * Create the title row with all the provided fields as column headings
 *
 * @param {Object} params Function parameters containing data, fields and delimiter
 * @returns {String} titles as a string
 */
function createColumnTitles(params) {
  var str = '';

  //if CSV has column title, then create it
  if (params.hasCSVColumnTitle) {
    params.fieldNames.forEach(function (element) {
      if (str !== '') {
        str += params.del;
      }
      str += JSON.stringify(element).replace(/\"/g, params.quotes);
    });
  }

  return str;
}

/**
 * Replace the quotation marks of the field element if needed (can be a not string-like item)
 *
 * @param {string} stringifiedElement The field element after JSON.stringify()
 * @param {string} quotes The params.quotes value. At this point we know that is not equal to double (")
 */
function replaceQuotationMarks(stringifiedElement, quotes) {
  var lastCharIndex = stringifiedElement.length - 1;

  //check if it's an string-like element
  if (stringifiedElement[0] === '"' && stringifiedElement[lastCharIndex] === '"') {
    //split the stringified field element because Strings are immutable
    var splitElement = stringifiedElement.split('');

    //replace the quotation marks
    splitElement[0] = quotes;
    splitElement[lastCharIndex] = quotes;

    //join again
    stringifiedElement = splitElement.join('');
  }

  return stringifiedElement;
}

/**
 * Create the content column by column and row by row below the title
 *
 * @param {Object} params Function parameters containing data, fields and delimiter
 * @param {String} str Title row as a string
 * @returns {String} csv string
 */
function createColumnContent(params, str) {
  params.data.forEach(function (dataElement) {
    //if null or empty object do nothing
    if (dataElement && Object.getOwnPropertyNames(dataElement).length > 0) {
      var line = '';
      var eol = params.newLine || os.EOL || '\n';

      params.fields.forEach(function (fieldElement) {
        var val;

        if (params.nested) {
          val = get(dataElement, fieldElement, '');
        } else {
          val = dataElement[fieldElement];
        }

        if (val !== undefined) {
          var stringifiedElement = JSON.stringify(val);

          if (params.quotes !== '"') {
            stringifiedElement = replaceQuotationMarks(stringifiedElement, params.quotes);
          }

          line += stringifiedElement;
        } else if (params.defaultValue !== undefined) {
          // Use default value if defined
          line += JSON.stringify(params.defaultValue);
        }

        line += params.del;
      });

      //remove last delimeter
      line = line.substring(0, line.length - 1);
      line = line.replace(/\\"/g, Array(3).join(params.quotes));
      //If header exists, add it, otherwise, print only content
      if (str !== '') {
        str += eol + line + params.eol;
      } else {
        str = line + params.eol;
      }
    }
  });

  return str;
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';

import '../css/Search.css';

class Search extends Component {

  render() {
    const { handleSubmit, handleChange, searchTerm } = this.props;
    return (
      <form onSubmit={handleSubmit} className="search-form">
        <input
          className="search-input"
          type="search"
          value={searchTerm}
          onChange={handleChange}
          placeholder="Enter keyword e.g 'Sports'"
        />
        <button className="submit-button" type="submit">Search</button>
      </form>
    );
  }

}

Search.propTypes = {
  searchTerm: PropTypes.string.isRequired,
  handleChange: PropTypes.func.isRequired,
  handleSubmit: PropTypes.func.isRequired
}

export default Search;
<commit_msg>Focus input on page load<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';

import '../css/Search.css';

class Search extends Component {

  render() {
    const { handleSubmit, handleChange, searchTerm } = this.props;
    return (
      <form onSubmit={handleSubmit} className="search-form">
        <input
          className="search-input"
          type="search"
          value={searchTerm}
          onChange={handleChange}
          placeholder="Enter keyword e.g 'Sports'"
          autoFocus
        />
        <button className="submit-button" type="submit">Search</button>
      </form>
    );
  }

}

Search.propTypes = {
  searchTerm: PropTypes.string.isRequired,
  handleChange: PropTypes.func.isRequired,
  handleSubmit: PropTypes.func.isRequired
}

export default Search;
<|endoftext|>


<commit_before>import React, { Component } from 'react';

import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';
import clamp from 'lodash.clamp';
import { toNumber } from './utils';

export default class Slider extends Component {
  static propTypes = {
    value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    min: PropTypes.number,
    max: PropTypes.number,
    width: PropTypes.number,
    onUpdate: PropTypes.func
  };

  state = {
    value: toNumber(this.props.value)
  };

  componentWillReceiveProps(nextProps) {
    this.setState({
      value: toNumber(nextProps.value)
    });
  }

  handleMouseDown = event => {
    this.update(event.pageX);

    window.addEventListener('mousemove', this.handleMouseMove);
    window.addEventListener('mouseup', this.handleMouseUp);
  };

  handleMouseMove = event => {
    this.update(event.pageX);

    event.preventDefault();
  };

  handleMouseUp = event => {
    this.update(event.pageX, false);

    window.removeEventListener('mousemove', this.handleMouseMove);
    window.removeEventListener('mouseup', this.handleMouseUp);
  };

  handleClick = event => {
    // do not focus input field on slider click
    event.preventDefault();
  };

  update(pageX, isLive = true) {
    const { min, max, onUpdate } = this.props;
    const rect = ReactDOM.findDOMNode(this).getBoundingClientRect();
    const x = pageX - rect.left;
    const w = rect.right - rect.left;
    const value = min + clamp(x / w, 0, 1) * (max - min);

    this.setState({ value }, () => {
      onUpdate(value, isLive);
    });
  }

  render() {
    const { min, max, width } = this.props;
    const widthBackground = clamp(
      ((this.state.value - min) * 100) / (max - min),
      0,
      100
    );
    const style = {
      width: `${width}%`,
      backgroundSize: `${widthBackground}% 100%`
    };

    return (
      <span
        className="slider"
        style={style}
        onClick={this.handleClick}
        onMouseDown={this.handleMouseDown}
      />
    );
  }
}
<commit_msg>Slider: Add default props, fix linting and accessibility issues, implement getDerivedStateFromProps<commit_after>/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/control-has-associated-label */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import clamp from 'lodash.clamp';
import { toNumber } from './utils';

export default class Slider extends Component {
  static propTypes = {
    value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
    min: PropTypes.number,
    max: PropTypes.number,
    width: PropTypes.number,
    onUpdate: PropTypes.func
  };

  static defaultProps = {
    value: null,
    min: null,
    max: null,
    width: null,
    onUpdate: () => null
  };

  constructor() {
    super();
    this.state = { value: null };
    this.sliderRef = React.createRef();
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    const nextValue = toNumber(nextProps.value);

    if (prevState.value === nextValue) return null;

    return {
      value: nextValue
    };
  }

  handleMouseDown = event => {
    this.update(event.pageX);

    window.addEventListener('mousemove', this.handleMouseMove);
    window.addEventListener('mouseup', this.handleMouseUp);
  };

  handleMouseMove = event => {
    this.update(event.pageX);

    event.preventDefault();
  };

  handleMouseUp = event => {
    this.update(event.pageX, false);

    window.removeEventListener('mousemove', this.handleMouseMove);
    window.removeEventListener('mouseup', this.handleMouseUp);
  };

  handleClick = event => {
    // do not focus input field on slider click
    event.preventDefault();
  };

  update(pageX, isLive = true) {
    const { min, max, onUpdate } = this.props;
    const rect = this.sliderRef.current.getBoundingClientRect();
    const x = pageX - rect.left;
    const w = rect.right - rect.left;
    const value = min + clamp(x / w, 0, 1) * (max - min);

    this.setState({ value }, () => {
      onUpdate(value, isLive);
    });
  }

  render() {
    const { min, max, width } = this.props;
    const { value } = this.state;
    const widthBackground = clamp(((value - min) * 100) / (max - min), 0, 100);
    const style = {
      width: `${width}%`,
      backgroundSize: `${widthBackground}% 100%`
    };

    return (
      <span
        ref={this.sliderRef}
        className="slider"
        style={style}
        onClick={this.handleClick}
        onMouseDown={this.handleMouseDown}
        role="slider"
        tabIndex={0}
        aria-valuenow={value}
        aria-valuemin={min}
        aria-valuemax={max}
      />
    );
  }
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';

import './Source.css';

import Select from './Select.js';
import AnnoText from './AnnoText.js';

import { getLastChunkAtTime } from '../util/chunk';

const languageOptions = [
  { value: 'ja', label: 'Japanese' },
  { value: 'en', label: 'English' },
];

// FileChooserForm
const FileChooser = ({ label, accept, onChoose }) => (
  {label} <input type="file" accept={accept} onChange={e => { onChoose(e.target.files[0]); e.target.value = null; }} />
);

// VideoImportControls
class VideoImportControls extends Component {
  render() {
    const { onImportVideoFile, onImportVideoURL, onImportSubsFile } = this.props;
    return (

        <form>
          <FileChooser label="Import Video File" accept="video/*" onChoose={(file) => { onImportVideoFile(file, this.videoFileLanguageVal); }} />
          <Select options={languageOptions} onSet={v => { this.videoFileLanguageVal = v; }} />
        </form>
        <form>
          Import Video URL <input type="text" placeholder="Video URL" onChange={(e) => { this.videoURLVal = e.target.value; }} />
          <Select options={languageOptions} onSet={v => { this.videoURLLanguageVal = v; }} />
          <button type="submit" onClick={(e) => { e.preventDefault(); onImportVideoURL(this.videoURLVal, this.videoURLLanguageVal); }}>Import</button>

        </form>
        <form>
          <FileChooser label="Import Subs (SRT)" accept=".srt" onChoose={(file) => { onImportSubsFile(file, this.subLanguageVal); }} />
          <Select options={languageOptions} onSet={v => { this.subLanguageVal = v; }} />
        </form>

    );
  }
}

class VideoMedia extends Component {
  constructor(props) {
    super(props);
    this.videoElem = null;
  }

  seek(t) {
    if (this.videoElem) {
      this.videoElem.currentTime = t;
    }
  }

  seekRelative(dt) {
    if (this.videoElem) {
      const nt = this.videoElem.currentTime + dt;
      this.videoElem.currentTime = nt >= 0 ? nt : 0;
    }
  }

  play() {
    this.videoElem.play();
  }

  pause() {
    this.videoElem.pause();
  }

  togglePause() {
    if (this.videoElem) {
      if (this.videoElem.paused) {
        this.videoElem.play();
      } else {
        this.videoElem.pause();
      }
    }
  }

  render() {
    const { media, initialTime, onTimeUpdate, onPlaying, onPause, onEnded, onSeeking } = this.props;
    return (
      <video src={media.first().videoURL} controls onTimeUpdate={e => { onTimeUpdate(e.target.currentTime); }} onPlaying={onPlaying} onPause={onPause} onEnded={onEnded} onSeeking={onSeeking} ref={(el) => { this.videoElem = el; }} onLoadedMetadata={e => { e.target.currentTime = initialTime ? initialTime : 0; }} />
    );
  }
}

class PlayControls extends Component {
  componentDidMount() {
    document.body.addEventListener('keydown', this.handleKeyDown);
  }

  componentWillUnmount() {
    document.body.removeEventListener('keydown', this.handleKeyDown);
  }

  handleKeyDown = (e) => {
    // Only process event if the target is the body,
    // to avoid messing with typing into input elements, etc.
    // Should we do this instead? e.target.tagName.toUpperCase() === 'INPUT'
    if (e.target !== document.body) {
      return;
    }

    const { onBack, onReplay, onTogglePause, onContinue } = this.props;

    if (!e.repeat) {
      switch (e.keyCode) {
        case 65: // a
          onBack();
          break;

        case 82: // r
          onReplay();
          break;

        case 32: // space
          onTogglePause();
          e.preventDefault();
          break;

        case 13: // ener
          onContinue();
          e.preventDefault();
          break;

        default:
          // ignore
          break;
      }
    }
  }

  render() {
    const { onBack, onReplay, onTogglePause, onContinue, onSetQuizMode } = this.props;
    return (
      <form className="PlayControls">
        <button type="button" onClick={onBack}>Jump Back [A]</button>
        <button type="button" onClick={onReplay}>Replay [R]</button>
        <button type="button" onClick={onTogglePause}>Play/Pause [Space]</button>
        <button type="button" onClick={onContinue}>Continue [Enter]</button>
        <Select options={[
          {value: 'none', label: 'None'},
          {value: 'listen', label: 'Listen'},
        ]} onSet={onSetQuizMode} />
      </form>
    );
  }
}

// Source
export default class Source extends Component {
  constructor(props) {
    super(props);
    this.videoMediaComponent = undefined;
    this.state = {
      textViewPosition: props.source.viewPosition,
      quizMode: 'none',
      quizPause: false, // are we paused (or have requested pause) for quiz?
      quizState: null,
      showingInfo: !props.source.media.size || !props.source.texts.size,
    };
    this.videoTime = null;
    this.videoIsPlaying = false;
  }

  handleImportVideoURL = (url, language) => {
    const {source, actions} = this.props;
    actions.sourceAddVideoURL(source.id, url, language);
  };

  handleImportVideoFile = (file, language) => {
    const {source, actions} = this.props;
    actions.sourceAddVideoFile(source.id, file, language);
  };

  handleImportSubsFile = (file, language) => {
    const {source, actions} = this.props;
    actions.sourceAddSubsFile(source.id, file, language);
  };

  handleVideoTimeUpdate = (time) => {
    this.videoTime = time;

    if (this.state.quizPause) {
      // We're either paused or in the process of pausing for question,
      //  so should ignore this time update.
      return;
    }

    const { source } = this.props;

    let pauseForQuiz = false;

    // Determine if we need to pause for a quiz
    // Is the video playing? Don't want to mis-trigger pause upon seeking
    if (this.videoIsPlaying) {
      // Is there at least one text track?
      if (this.state.quizMode === 'none') {
      } else if (this.state.quizMode === 'listen') {
        if (source.texts.size >= 1) {
          const firstText = source.texts.first();

          // Look up chunk (if any) before this time change
          const currentChunk = getLastChunkAtTime(firstText.chunkSet, this.state.textViewPosition);

          if (currentChunk) {
            // Are we passing the time that would trigger a pause for quiz?
            const PAUSE_DELAY = 0.3;
            const triggerTime = currentChunk.position.end - PAUSE_DELAY;

            if ((this.state.textViewPosition < triggerTime) && (time >= triggerTime)) {
              pauseForQuiz = true;
              this.setState({
                quizState: {
                  textRevelation: 0,
                }
              });
            }
          }
        }
      } else {
        throw new Error('internal error');
      }
    }

    if (pauseForQuiz) {
      this.setState({
        quizPause: true,
      });
      this.videoMediaComponent.pause();
    } else {
      this.setState({textViewPosition: time});
    }
  };

  releaseQuizPause = () => {
    this.setState({
      quizPause: false,
      // Resync displayed text with video time, since they may have gotten
      //  very slightly out of sync if we were paused for question
      textViewPosition: this.videoTime,
    });
  }

  handleVideoPlaying = () => {
    this.videoIsPlaying = true;
  };

  handleVideoPause = () => {
    this.videoIsPlaying = false;
  };

  handleVideoEnded = () => {
    this.videoIsPlaying = false;
  };

  handleVideoSeeking = () => {
    this.videoIsPlaying = false;
    this.releaseQuizPause();
  };

  handleSetQuizMode = (mode) => {
    switch (mode) {
      case 'none':
        this.setState({
          quizMode: mode,
          quizPause: false,
          quizState: null,
        });
        break;

      case 'listen':
        this.setState({
          quizMode: mode,
          quizPause: false,
          quizState: null,
        });
        break;

      default:
        throw new Error('internal error');
    }
  }

  handleBack = () => {
    if (this.videoMediaComponent) {
      this.videoMediaComponent.seekRelative(-3.0);
    }
  };

  handleReplay = () => {
    if (this.videoMediaComponent) {
      const firstText = this.props.source.texts.first();
      const currentChunk = getLastChunkAtTime(firstText.chunkSet, this.state.textViewPosition);

      if (currentChunk) {
        this.videoMediaComponent.seek(currentChunk.position.begin);
        this.videoMediaComponent.play();
      }
    }
  };

  handleTogglePause = () => {
    this.releaseQuizPause();
    if (this.videoMediaComponent) {
      this.videoMediaComponent.togglePause();
    }
  };

  handleContinue = () => {
    switch (this.state.quizMode) {
      case 'none':
        // ignore
        break;

      case 'listen':
        if (this.state.quizPause) {
          switch (this.state.quizState.textRevelation) {
            case 0:
              this.setState((s) => { s.quizState.textRevelation++; });
              break;

            case 1:
              this.setState((s) => { s.quizState.textRevelation++; });
              break;

            case 2:
              // Continue playing video
              this.videoMediaComponent.play();
              this.releaseQuizPause();
              this.setState({
                quizState: null,
              });
              break;

            default:
              throw new Error('internal error');
          }
        }
        break;

      default:
        throw new Error('internal error');
    }
  };

  handleToggleInfo = () => {
    this.setState(state => ({ ...state, showingInfo: !state.showingInfo}));
  }

  handleExit = () => {
    // TODO: this should probably be last-reported time,
    //  slightly different than textViewPosition?
    this.props.onUpdateViewPosition(this.state.textViewPosition);
    this.props.onExit();
  }

  render() {
    const { source, highlightSets, activeSetId, onSetActiveSetId, onSetChunkAnnoText, onDeleteSource, onDeleteMedia, onDeleteText } = this.props;

    // Based on quiz mode and state, determine what texts are shown
    let showFirstText = true;
    let showRestTexts = true;
    switch (this.state.quizMode) {
      case 'none':
        // don't change
        break;

      case 'listen':
        if (this.state.quizPause) {
          switch (this.state.quizState.textRevelation) {
            case 0:
              showFirstText = false;
              showRestTexts = false;
              break;

            case 1:
              showFirstText = true;
              showRestTexts = false;
              break;

            case 2:
              showFirstText = true;
              showRestTexts = true;
              break;

            default:
              throw new Error('internal error');
          }
        } else {
          showFirstText = false;
          showRestTexts = false;
        }
        break;

      default:
        throw new Error('internal error');
    }

    return (

        {source.media.size ? (

              <VideoMedia media={source.media} initialTime={this.props.source.viewPosition} onTimeUpdate={this.handleVideoTimeUpdate} onPlaying={this.handleVideoPlaying} onPause={this.handleVideoPause} onEnded={this.handleVideoEnded} onSeeking={this.handleVideoSeeking} ref={(c) => { this.videoMediaComponent = c; }} />

                {source.texts.map((text, textNum) => {
                  const chunk = getLastChunkAtTime(text.chunkSet, this.state.textViewPosition);

                  if (chunk) {
                    return (

                          {(() => {
                            const textHidden = ((textNum === 0) && !showFirstText) || ((textNum > 0) && !showRestTexts);

                            return (

                                {textHidden ? (

(hidden)

                                ) : null}

                                  <AnnoText key={chunk.uid} annoText={chunk.annoText} language={text.language} onUpdate={newAnnoText => { onSetChunkAnnoText(textNum, chunk.uid, newAnnoText); }} highlightSets={highlightSets} activeSetId={activeSetId} onSetActiveSetId={onSetActiveSetId} />

                            );
                          })()}

                    );
                  } else {
                    return null;
                  }
                })}

            <PlayControls onBack={this.handleBack} onReplay={this.handleReplay} onTogglePause={this.handleTogglePause} onContinue={this.handleContinue} onSetQuizMode={this.handleSetQuizMode} />

        ) : null}

Id: {source.id} <button onClick={() => { if (window.confirm('Delete source "' + source.name + '"?')) { onDeleteSource(); } }}>Delete Source</button>

Name: <input ref={(el) => { this.nameInputElem = el; }} type="text" defaultValue={source.name} /> <button onClick={() => { this.props.onSetName(this.nameInputElem.value); }}>Set</button>

Kind: {source.kind}

          <VideoImportControls onImportVideoURL={this.handleImportVideoURL} onImportVideoFile={this.handleImportVideoFile} onImportSubsFile={this.handleImportSubsFile} />

Media:

{source.media.map((o, i) => (

#{i} [{o.language}]
              <button onClick={() => { if (window.confirm('Delete media?')) { onDeleteMedia(i); } }}>Delete</button>

          ))}

Texts:

{source.texts.map((o, i) => (

#{i} [{o.language}]
              <button onClick={() => { if (window.confirm('Delete text?')) { onDeleteText(i); } }}>Delete</button>

          ))}

        <button className="Source-big-button Source-exit-button" onClick={this.handleExit}>↩</button>
        <button className="Source-big-button Source-toggle-info-button" onClick={this.handleToggleInfo}>ⓘ</button>

    );
  }
}
<commit_msg>Vertically center "hidden" label<commit_after>import React, { Component } from 'react';

import './Source.css';

import Select from './Select.js';
import AnnoText from './AnnoText.js';

import { getLastChunkAtTime } from '../util/chunk';

const languageOptions = [
  { value: 'ja', label: 'Japanese' },
  { value: 'en', label: 'English' },
];

// FileChooserForm
const FileChooser = ({ label, accept, onChoose }) => (
  {label} <input type="file" accept={accept} onChange={e => { onChoose(e.target.files[0]); e.target.value = null; }} />
);

// VideoImportControls
class VideoImportControls extends Component {
  render() {
    const { onImportVideoFile, onImportVideoURL, onImportSubsFile } = this.props;
    return (

        <form>
          <FileChooser label="Import Video File" accept="video/*" onChoose={(file) => { onImportVideoFile(file, this.videoFileLanguageVal); }} />
          <Select options={languageOptions} onSet={v => { this.videoFileLanguageVal = v; }} />
        </form>
        <form>
          Import Video URL <input type="text" placeholder="Video URL" onChange={(e) => { this.videoURLVal = e.target.value; }} />
          <Select options={languageOptions} onSet={v => { this.videoURLLanguageVal = v; }} />
          <button type="submit" onClick={(e) => { e.preventDefault(); onImportVideoURL(this.videoURLVal, this.videoURLLanguageVal); }}>Import</button>

        </form>
        <form>
          <FileChooser label="Import Subs (SRT)" accept=".srt" onChoose={(file) => { onImportSubsFile(file, this.subLanguageVal); }} />
          <Select options={languageOptions} onSet={v => { this.subLanguageVal = v; }} />
        </form>

    );
  }
}

class VideoMedia extends Component {
  constructor(props) {
    super(props);
    this.videoElem = null;
  }

  seek(t) {
    if (this.videoElem) {
      this.videoElem.currentTime = t;
    }
  }

  seekRelative(dt) {
    if (this.videoElem) {
      const nt = this.videoElem.currentTime + dt;
      this.videoElem.currentTime = nt >= 0 ? nt : 0;
    }
  }

  play() {
    this.videoElem.play();
  }

  pause() {
    this.videoElem.pause();
  }

  togglePause() {
    if (this.videoElem) {
      if (this.videoElem.paused) {
        this.videoElem.play();
      } else {
        this.videoElem.pause();
      }
    }
  }

  render() {
    const { media, initialTime, onTimeUpdate, onPlaying, onPause, onEnded, onSeeking } = this.props;
    return (
      <video src={media.first().videoURL} controls onTimeUpdate={e => { onTimeUpdate(e.target.currentTime); }} onPlaying={onPlaying} onPause={onPause} onEnded={onEnded} onSeeking={onSeeking} ref={(el) => { this.videoElem = el; }} onLoadedMetadata={e => { e.target.currentTime = initialTime ? initialTime : 0; }} />
    );
  }
}

class PlayControls extends Component {
  componentDidMount() {
    document.body.addEventListener('keydown', this.handleKeyDown);
  }

  componentWillUnmount() {
    document.body.removeEventListener('keydown', this.handleKeyDown);
  }

  handleKeyDown = (e) => {
    // Only process event if the target is the body,
    // to avoid messing with typing into input elements, etc.
    // Should we do this instead? e.target.tagName.toUpperCase() === 'INPUT'
    if (e.target !== document.body) {
      return;
    }

    const { onBack, onReplay, onTogglePause, onContinue } = this.props;

    if (!e.repeat) {
      switch (e.keyCode) {
        case 65: // a
          onBack();
          break;

        case 82: // r
          onReplay();
          break;

        case 32: // space
          onTogglePause();
          e.preventDefault();
          break;

        case 13: // ener
          onContinue();
          e.preventDefault();
          break;

        default:
          // ignore
          break;
      }
    }
  }

  render() {
    const { onBack, onReplay, onTogglePause, onContinue, onSetQuizMode } = this.props;
    return (
      <form className="PlayControls">
        <button type="button" onClick={onBack}>Jump Back [A]</button>
        <button type="button" onClick={onReplay}>Replay [R]</button>
        <button type="button" onClick={onTogglePause}>Play/Pause [Space]</button>
        <button type="button" onClick={onContinue}>Continue [Enter]</button>
        <Select options={[
          {value: 'none', label: 'None'},
          {value: 'listen', label: 'Listen'},
        ]} onSet={onSetQuizMode} />
      </form>
    );
  }
}

// Source
export default class Source extends Component {
  constructor(props) {
    super(props);
    this.videoMediaComponent = undefined;
    this.state = {
      textViewPosition: props.source.viewPosition,
      quizMode: 'none',
      quizPause: false, // are we paused (or have requested pause) for quiz?
      quizState: null,
      showingInfo: !props.source.media.size || !props.source.texts.size,
    };
    this.videoTime = null;
    this.videoIsPlaying = false;
  }

  handleImportVideoURL = (url, language) => {
    const {source, actions} = this.props;
    actions.sourceAddVideoURL(source.id, url, language);
  };

  handleImportVideoFile = (file, language) => {
    const {source, actions} = this.props;
    actions.sourceAddVideoFile(source.id, file, language);
  };

  handleImportSubsFile = (file, language) => {
    const {source, actions} = this.props;
    actions.sourceAddSubsFile(source.id, file, language);
  };

  handleVideoTimeUpdate = (time) => {
    this.videoTime = time;

    if (this.state.quizPause) {
      // We're either paused or in the process of pausing for question,
      //  so should ignore this time update.
      return;
    }

    const { source } = this.props;

    let pauseForQuiz = false;

    // Determine if we need to pause for a quiz
    // Is the video playing? Don't want to mis-trigger pause upon seeking
    if (this.videoIsPlaying) {
      // Is there at least one text track?
      if (this.state.quizMode === 'none') {
      } else if (this.state.quizMode === 'listen') {
        if (source.texts.size >= 1) {
          const firstText = source.texts.first();

          // Look up chunk (if any) before this time change
          const currentChunk = getLastChunkAtTime(firstText.chunkSet, this.state.textViewPosition);

          if (currentChunk) {
            // Are we passing the time that would trigger a pause for quiz?
            const PAUSE_DELAY = 0.3;
            const triggerTime = currentChunk.position.end - PAUSE_DELAY;

            if ((this.state.textViewPosition < triggerTime) && (time >= triggerTime)) {
              pauseForQuiz = true;
              this.setState({
                quizState: {
                  textRevelation: 0,
                }
              });
            }
          }
        }
      } else {
        throw new Error('internal error');
      }
    }

    if (pauseForQuiz) {
      this.setState({
        quizPause: true,
      });
      this.videoMediaComponent.pause();
    } else {
      this.setState({textViewPosition: time});
    }
  };

  releaseQuizPause = () => {
    this.setState({
      quizPause: false,
      // Resync displayed text with video time, since they may have gotten
      //  very slightly out of sync if we were paused for question
      textViewPosition: this.videoTime,
    });
  }

  handleVideoPlaying = () => {
    this.videoIsPlaying = true;
  };

  handleVideoPause = () => {
    this.videoIsPlaying = false;
  };

  handleVideoEnded = () => {
    this.videoIsPlaying = false;
  };

  handleVideoSeeking = () => {
    this.videoIsPlaying = false;
    this.releaseQuizPause();
  };

  handleSetQuizMode = (mode) => {
    switch (mode) {
      case 'none':
        this.setState({
          quizMode: mode,
          quizPause: false,
          quizState: null,
        });
        break;

      case 'listen':
        this.setState({
          quizMode: mode,
          quizPause: false,
          quizState: null,
        });
        break;

      default:
        throw new Error('internal error');
    }
  }

  handleBack = () => {
    if (this.videoMediaComponent) {
      this.videoMediaComponent.seekRelative(-3.0);
    }
  };

  handleReplay = () => {
    if (this.videoMediaComponent) {
      const firstText = this.props.source.texts.first();
      const currentChunk = getLastChunkAtTime(firstText.chunkSet, this.state.textViewPosition);

      if (currentChunk) {
        this.videoMediaComponent.seek(currentChunk.position.begin);
        this.videoMediaComponent.play();
      }
    }
  };

  handleTogglePause = () => {
    this.releaseQuizPause();
    if (this.videoMediaComponent) {
      this.videoMediaComponent.togglePause();
    }
  };

  handleContinue = () => {
    switch (this.state.quizMode) {
      case 'none':
        // ignore
        break;

      case 'listen':
        if (this.state.quizPause) {
          switch (this.state.quizState.textRevelation) {
            case 0:
              this.setState((s) => { s.quizState.textRevelation++; });
              break;

            case 1:
              this.setState((s) => { s.quizState.textRevelation++; });
              break;

            case 2:
              // Continue playing video
              this.videoMediaComponent.play();
              this.releaseQuizPause();
              this.setState({
                quizState: null,
              });
              break;

            default:
              throw new Error('internal error');
          }
        }
        break;

      default:
        throw new Error('internal error');
    }
  };

  handleToggleInfo = () => {
    this.setState(state => ({ ...state, showingInfo: !state.showingInfo}));
  }

  handleExit = () => {
    // TODO: this should probably be last-reported time,
    //  slightly different than textViewPosition?
    this.props.onUpdateViewPosition(this.state.textViewPosition);
    this.props.onExit();
  }

  render() {
    const { source, highlightSets, activeSetId, onSetActiveSetId, onSetChunkAnnoText, onDeleteSource, onDeleteMedia, onDeleteText } = this.props;

    // Based on quiz mode and state, determine what texts are shown
    let showFirstText = true;
    let showRestTexts = true;
    switch (this.state.quizMode) {
      case 'none':
        // don't change
        break;

      case 'listen':
        if (this.state.quizPause) {
          switch (this.state.quizState.textRevelation) {
            case 0:
              showFirstText = false;
              showRestTexts = false;
              break;

            case 1:
              showFirstText = true;
              showRestTexts = false;
              break;

            case 2:
              showFirstText = true;
              showRestTexts = true;
              break;

            default:
              throw new Error('internal error');
          }
        } else {
          showFirstText = false;
          showRestTexts = false;
        }
        break;

      default:
        throw new Error('internal error');
    }

    return (

        {source.media.size ? (

              <VideoMedia media={source.media} initialTime={this.props.source.viewPosition} onTimeUpdate={this.handleVideoTimeUpdate} onPlaying={this.handleVideoPlaying} onPause={this.handleVideoPause} onEnded={this.handleVideoEnded} onSeeking={this.handleVideoSeeking} ref={(c) => { this.videoMediaComponent = c; }} />

                {source.texts.map((text, textNum) => {
                  const chunk = getLastChunkAtTime(text.chunkSet, this.state.textViewPosition);

                  if (chunk) {
                    return (

                          {(() => {
                            const textHidden = ((textNum === 0) && !showFirstText) || ((textNum > 0) && !showRestTexts);

                            return (

                                {textHidden ? (

(hidden)

                                ) : null}

                                  <AnnoText key={chunk.uid} annoText={chunk.annoText} language={text.language} onUpdate={newAnnoText => { onSetChunkAnnoText(textNum, chunk.uid, newAnnoText); }} highlightSets={highlightSets} activeSetId={activeSetId} onSetActiveSetId={onSetActiveSetId} />

                            );
                          })()}

                    );
                  } else {
                    return null;
                  }
                })}

            <PlayControls onBack={this.handleBack} onReplay={this.handleReplay} onTogglePause={this.handleTogglePause} onContinue={this.handleContinue} onSetQuizMode={this.handleSetQuizMode} />

        ) : null}

Id: {source.id} <button onClick={() => { if (window.confirm('Delete source "' + source.name + '"?')) { onDeleteSource(); } }}>Delete Source</button>

Name: <input ref={(el) => { this.nameInputElem = el; }} type="text" defaultValue={source.name} /> <button onClick={() => { this.props.onSetName(this.nameInputElem.value); }}>Set</button>

Kind: {source.kind}

          <VideoImportControls onImportVideoURL={this.handleImportVideoURL} onImportVideoFile={this.handleImportVideoFile} onImportSubsFile={this.handleImportSubsFile} />

Media:

{source.media.map((o, i) => (

#{i} [{o.language}]
              <button onClick={() => { if (window.confirm('Delete media?')) { onDeleteMedia(i); } }}>Delete</button>

          ))}

Texts:

{source.texts.map((o, i) => (

#{i} [{o.language}]
              <button onClick={() => { if (window.confirm('Delete text?')) { onDeleteText(i); } }}>Delete</button>

          ))}

        <button className="Source-big-button Source-exit-button" onClick={this.handleExit}>↩</button>
        <button className="Source-big-button Source-toggle-info-button" onClick={this.handleToggleInfo}>ⓘ</button>

    );
  }
}
<|endoftext|>


<commit_before>const KEYSTORE_NAME = "__holly_keystore";

export class Keystore {
  constructor(db) {
    this.db = db;
  }

  getAll() {
    return this.db(KEYSTORE_NAME).size() > 0 ? this.db(KEYSTORE_NAME).first() : {};
  }

  get(key) {
    let keystore = this.getAll();
    return keystore[key];
  }

  set(obj) {
    let keystore = this.getAll();
    Object.assign(keystore, obj);
    this.db(KEYSTORE_NAME).splice(0, this.db(KEYSTORE_NAME).size(), keystore);
  }

  reset() {
    this.db(KEYSTORE_NAME).remove();
  }
}
<commit_msg>remove holly from keystore<commit_after>const KEYSTORE_NAME = "_keystore";

export class Keystore {
  constructor(db) {
    this.db = db;
  }

  getAll() {
    return this.db(KEYSTORE_NAME).size() > 0 ? this.db(KEYSTORE_NAME).first() : {};
  }

  get(key) {
    let keystore = this.getAll();
    return keystore[key];
  }

  set(obj) {
    let keystore = this.getAll();
    Object.assign(keystore, obj);
    this.db(KEYSTORE_NAME).splice(0, this.db(KEYSTORE_NAME).size(), keystore);
  }

  reset() {
    this.db(KEYSTORE_NAME).remove();
  }
}
<|endoftext|>


<commit_before>import React, { Component, PropTypes } from 'react'
import {
  cloneDeep, isUndefined, pullAt, findKey, find, has, isString, isFunction
} from 'lodash'
import pluralize from 'pluralize'
import defaultFromStruct from '../utils/defaultFromStruct'
import autoBind from '../utils/autoBind'
import warning from '../utils/warning'

import Accordion from './Accordion'
import Panel from './Panel'
import Input from './Input'
import ObjectEditor from './ObjectEditor'

const CollectionTypes = [ 'KeyValue', 'Object', 'Collection', 'Array' ]

class Struct extends Component {

  constructor() {
    super()

    this.state = {
      struct: {}
    }

    autoBind(this, [
      'updateStateKey', 'updateStateIndex', 'updateCollection', 'updateCollectionOrder',
      'updateState', 'addToCollection', 'removeFromCollection', 'handleRemoveClick',
      'renderNonCollections', 'renderCollection', 'renderCollections'
    ])
  }

  componentWillMount() {
    const { struct, data } = this.props
    this.setState({ struct: Object.assign({}, defaultFromStruct(struct.struct), data) })
  }

  componentWillReceiveProps(nextProps) {
    this.setState({ struct: Object.assign({}, this.state.struct, nextProps.data) })
  }

  updateStateKey(key, val) {
    this.setState({ struct: Object.assign({}, this.state.struct, { [key]: val }) })
  }

  updateStateIndex(key, index, val) {
    const { struct } = this.state
    const data = cloneDeep(struct[key])
    data[index] = val
    struct[key] = data
    this.setState({ struct })
  }

  updateCollection(key, val, index) {
    let needsUpdate = false
    const data = cloneDeep(this.state.struct[key])
    const current = cloneDeep(data[index])

    let { struct } = this.props.struct
    if (isUndefined(struct)) {
      struct = this.props.struct
    }
    const idAttr = findKey(struct[key].struct, { type: 'Id' })
    if (!isUndefined(idAttr) && current[idAttr] !== val[idAttr]) {
      if (!isUndefined(find(data, { [idAttr]: val[idAttr] }))) {
        alert(`Id types must be unique. (${idAttr})`); // eslint-disable-line
        return
      }
      needsUpdate = true
    }

    data[index] = val
    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
    if (needsUpdate) {
      this.props.updateRef({ currentId: current[idAttr], newId: val[idAttr] })
    }
  }

  updateCollectionOrder(key, newOrder) {
    const collection = cloneDeep(this.state.struct[key])

    const data = []
    for (const order of newOrder) {
      data.push(collection[parseInt(order, 10)])
    }

    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
  }

  updateState() {
    this.props.updateData(this.state.struct, this.props.index)
  }

  addToCollection(key) {
    const { struct } = this.props
    const data = cloneDeep(this.state.struct[key])
    let s = struct
    if (has(s, key) && has(s[key], 'type') && (s[key].type === 'KeyValue' || s[key].type === 'Object')) {
      data[''] = undefined
    } else if (has(s, key) && has(s[key], 'type') && s[key].type === 'Array') {
      data.push(s[key].struct.default)
    } else if (has(s, key) && has(s[key], 'type')) {
      data.push(defaultFromStruct(s[key].struct))
    } else {
      s = s.struct
      if (has(s, key) && has(s[key], 'type') && (s[key].type === 'KeyValue' || s[key].type === 'Object')) {
        data[''] = undefined
      } else if (has(s, key) && has(s[key], 'type') && s[key].type === 'Array') {
        data.push(s[key].struct.default)
      } else if (has(s, key) && has(s[key], 'type')) {
        data.push(defaultFromStruct(s[key].struct))
      } else {
        data.push(defaultFromStruct(s[key].struct))
      }
    }
    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
  }

  removeFromCollection(key, index) {
    const { struct } = this.state
    const data = cloneDeep(struct[key])
    pullAt(data, [ index ])
    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
  }

  handleRemoveClick() {
    const { removeFromCollection, attr, index } = this.props
    removeFromCollection(attr, index)
  }

  renderNonCollections({ struct, hideAfter }) {
    let beforeHide = -1
    if (!isUndefined(hideAfter)) { beforeHide = hideAfter }
    if (!isUndefined(struct)) {
      let keys = Object.keys(struct)
      const isNonCollection = (key) => {
        const notCollection = (k) => (CollectionTypes.indexOf(struct[k].type) < 0)
        return struct[key].type.indexOf('Collection') < 0 && notCollection(key)
      }
      keys = keys.filter(isNonCollection)

      const rendered = []
      if (beforeHide !== -1) {
        keys.slice(0, beforeHide + 1).map((key, i) => {
          rendered.push((

              {struct[key].label}

              <Input
                name={key}
                struct={struct[key]}
                value={this.state.struct[key]}
                onChange={this.updateStateKey}
                optionTypes={this.props.optionTypes}
              />

          ))
        })
        const insidePanel = []
        keys.slice(beforeHide + 1).map((key, i) => {
          insidePanel.push((

              {struct[key].label}

              <Input
                name={key}
                struct={struct[key]}
                value={this.state.struct[key]}
                onChange={this.updateStateKey}
                optionTypes={this.props.optionTypes}
              />

          ))
        })
        rendered.push((
          <Accordion key={beforeHide + 1}>
            <Panel header="Other">
              {insidePanel}
            </Panel>
          </Accordion>
        ))
        return rendered
      }
      return keys.map((key, i) => (

          {struct[key].label}

          <Input
            name={key}
            struct={struct[key]}
            value={this.state.struct[key]}
            onChange={this.updateStateKey}
            optionTypes={this.props.optionTypes}
          />

      ))
    }
  }

  renderCollection(struct, key) {
    const { updateRef, optionTypes, collectionTypes } = this.props
    if (struct.type === 'Collection') {
      return this.state.struct[key].map((value, i) => (
        <Struct
          key={i}
          attr={key}
          data={value}
          struct={struct}
          updateRef={updateRef}
          optionTypes={optionTypes}
          collectionTypes={collectionTypes}
          removeFromCollection={this.removeFromCollection}
          updateData={this.updateCollection.bind(this, key)}
        />
      ))
    } else if (struct.type.indexOf('Collection') > -1) {
      const Comp = collectionTypes[struct.type.replace('Collection', '')]
      if (!isUndefined(Comp)) {
        return this.state.struct[key].map((value, i) => (
          <Comp
            key={i}
            attr={key}
            data={value}
            struct={struct}
            optionTypes={optionTypes}
            collectionTypes={collectionTypes}
            removeFromCollection={this.removeFromCollection}
            updateData={this.updateCollection.bind(this, key)}
          />
        ))
      }
    } else if (struct.type === 'KeyValue' || struct.type === 'Object') {
      return (
        <ObjectEditor
          name={key}
          struct={struct}
          optionTypes={optionTypes}
          value={this.state.struct[key]}
          onChange={this.updateStateKey}
          collectionTypes={collectionTypes}
          updateCollectionOrder={this.updateCollectionOrder}
        />
      )
    } else if (struct.type === 'Array') {
      return this.state.struct[key].map((val, i) => (
        <Panel
          key={i}
          header={`${i + 1} - ${val}`}
        >

            {struct.struct.label}

            <Input
              name={i}
              value={val}
              struct={struct.struct}
              onChange={this.updateStateIndex.bind(this, key)}
              optionTypes={this.props.optionTypes}
            />

          <button onClick={this.removeFromCollection.bind(this, key, i)}>
            Remove {pluralize(struct.struct.label, 1)}
          </button>
        </Panel>
      ))
    }
  }

  renderCollections({ struct }) {
    if (!isUndefined(struct)) {
      const isObject = (key) => (CollectionTypes.indexOf(struct[key].type) > -1)
      let keys = Object.keys(struct)
      const isCollection = (key) => (
        struct[key].type.indexOf('Collection') > -1 || isObject(key)
      )
      keys = keys.filter(isCollection)
      if (keys.length > 0) {
        return (
          <Accordion>
            {
              keys.map((key, i) => {
                const data = this.state.struct[key]
                let len = '-1'
                if (data) {
                  len = isObject(key) ? Object.keys(data).length : data.length
                }
                return (
                  <Panel
                    key={i}
                    header={`${struct[key].label} (${len})`}
                  >
                    <Accordion
                      orderable={has(struct[key], 'orderable') ? struct[key].orderable : true}
                      reorder={this.updateCollectionOrder.bind(this, key)}
                    >
                      {this.renderCollection(struct[key], key)}
                    </Accordion>
                    {
                      struct[key].type !== 'Object' &&
                      (
                        <button onClick={this.addToCollection.bind(this, key)}>
                          Add a {pluralize(struct[key].label, 1)}
                        </button>
                      )
                    }
                  </Panel>
                )
              })
            }
          </Accordion>
        )
      }
    }
  }

  render() {
    const { struct } = this.props
    let header = 'No header attr update struct.'
    if (has(struct, 'header')) {
      if (isString(struct.header) && has(this.state.struct, struct.header)) {
        header = this.state.struct[struct.header]
      } else if (isFunction(struct.header)) {
        header = struct.header(this.state.struct, parseInt(this.props.index, 10))
      }
    }
    if (isUndefined(struct.label)) {
      warning('undefined label?', struct)
    }
    if (struct.type === 'Object') {
      return (

          {this.renderNonCollections(struct || {})}
          {this.renderCollections(struct || {})}

          <button onClick={this.updateState}>
            Update {pluralize(struct.label, 1)}
          </button>

      )
    }
    return (
      <Panel header={header} {...this.props}>
        {this.renderNonCollections(struct || {})}
        {this.renderCollections(struct || {})}

        <button onClick={this.updateState}>
          Update {pluralize(struct.label, 1)}
        </button>
        <button onClick={this.handleRemoveClick}>
          Remove {pluralize(struct.label, 1)}
        </button>
      </Panel>
    )
  }
}

Struct.propTypes = {
  data: PropTypes.any,
  attr: PropTypes.any,
  struct: PropTypes.any,
  updateRef: PropTypes.func,
  updateData: PropTypes.func,
  optionTypes: PropTypes.object,
  collectionTypes: PropTypes.object,
  removeFromCollection: PropTypes.func,

  index: PropTypes.any,
  isOpen: PropTypes.any,
  openPanel: PropTypes.func
}

export default Struct
<commit_msg>Revert "use base struct if not found"<commit_after>import React, { Component, PropTypes } from 'react'
import {
  cloneDeep, isUndefined, pullAt, findKey, find, has, isString, isFunction
} from 'lodash'
import pluralize from 'pluralize'
import defaultFromStruct from '../utils/defaultFromStruct'
import autoBind from '../utils/autoBind'
import warning from '../utils/warning'

import Accordion from './Accordion'
import Panel from './Panel'
import Input from './Input'
import ObjectEditor from './ObjectEditor'

const CollectionTypes = [ 'KeyValue', 'Object', 'Collection', 'Array' ]

class Struct extends Component {

  constructor() {
    super()

    this.state = {
      struct: {}
    }

    autoBind(this, [
      'updateStateKey', 'updateStateIndex', 'updateCollection', 'updateCollectionOrder',
      'updateState', 'addToCollection', 'removeFromCollection', 'handleRemoveClick',
      'renderNonCollections', 'renderCollection', 'renderCollections'
    ])
  }

  componentWillMount() {
    const { struct, data } = this.props
    this.setState({ struct: Object.assign({}, defaultFromStruct(struct.struct), data) })
  }

  componentWillReceiveProps(nextProps) {
    this.setState({ struct: Object.assign({}, this.state.struct, nextProps.data) })
  }

  updateStateKey(key, val) {
    this.setState({ struct: Object.assign({}, this.state.struct, { [key]: val }) })
  }

  updateStateIndex(key, index, val) {
    const { struct } = this.state
    const data = cloneDeep(struct[key])
    data[index] = val
    struct[key] = data
    this.setState({ struct })
  }

  updateCollection(key, val, index) {
    let needsUpdate = false
    const data = cloneDeep(this.state.struct[key])
    const current = cloneDeep(data[index])

    const { struct } = this.props.struct
    const idAttr = findKey(struct[key].struct, { type: 'Id' })
    if (!isUndefined(idAttr) && current[idAttr] !== val[idAttr]) {
      if (!isUndefined(find(data, { [idAttr]: val[idAttr] }))) {
        alert(`Id types must be unique. (${idAttr})`); // eslint-disable-line
        return
      }
      needsUpdate = true
    }

    data[index] = val
    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
    if (needsUpdate) {
      this.props.updateRef({ currentId: current[idAttr], newId: val[idAttr] })
    }
  }

  updateCollectionOrder(key, newOrder) {
    const collection = cloneDeep(this.state.struct[key])

    const data = []
    for (const order of newOrder) {
      data.push(collection[parseInt(order, 10)])
    }

    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
  }

  updateState() {
    this.props.updateData(this.state.struct, this.props.index)
  }

  addToCollection(key) {
    const { struct } = this.props
    const data = cloneDeep(this.state.struct[key])
    let s = struct
    if (has(s, key) && has(s[key], 'type') && (s[key].type === 'KeyValue' || s[key].type === 'Object')) {
      data[''] = undefined
    } else if (has(s, key) && has(s[key], 'type') && s[key].type === 'Array') {
      data.push(s[key].struct.default)
    } else if (has(s, key) && has(s[key], 'type')) {
      data.push(defaultFromStruct(s[key].struct))
    } else {
      s = s.struct
      if (has(s, key) && has(s[key], 'type') && (s[key].type === 'KeyValue' || s[key].type === 'Object')) {
        data[''] = undefined
      } else if (has(s, key) && has(s[key], 'type') && s[key].type === 'Array') {
        data.push(s[key].struct.default)
      } else if (has(s, key) && has(s[key], 'type')) {
        data.push(defaultFromStruct(s[key].struct))
      } else {
        data.push(defaultFromStruct(s[key].struct))
      }
    }
    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
  }

  removeFromCollection(key, index) {
    const { struct } = this.state
    const data = cloneDeep(struct[key])
    pullAt(data, [ index ])
    this.props.updateData(Object.assign({}, this.state.struct, { [key]: data }), this.props.index)
  }

  handleRemoveClick() {
    const { removeFromCollection, attr, index } = this.props
    removeFromCollection(attr, index)
  }

  renderNonCollections({ struct, hideAfter }) {
    let beforeHide = -1
    if (!isUndefined(hideAfter)) { beforeHide = hideAfter }
    if (!isUndefined(struct)) {
      let keys = Object.keys(struct)
      const isNonCollection = (key) => {
        const notCollection = (k) => (CollectionTypes.indexOf(struct[k].type) < 0)
        return struct[key].type.indexOf('Collection') < 0 && notCollection(key)
      }
      keys = keys.filter(isNonCollection)

      const rendered = []
      if (beforeHide !== -1) {
        keys.slice(0, beforeHide + 1).map((key, i) => {
          rendered.push((

              {struct[key].label}

              <Input
                name={key}
                struct={struct[key]}
                value={this.state.struct[key]}
                onChange={this.updateStateKey}
                optionTypes={this.props.optionTypes}
              />

          ))
        })
        const insidePanel = []
        keys.slice(beforeHide + 1).map((key, i) => {
          insidePanel.push((

              {struct[key].label}

              <Input
                name={key}
                struct={struct[key]}
                value={this.state.struct[key]}
                onChange={this.updateStateKey}
                optionTypes={this.props.optionTypes}
              />

          ))
        })
        rendered.push((
          <Accordion key={beforeHide + 1}>
            <Panel header="Other">
              {insidePanel}
            </Panel>
          </Accordion>
        ))
        return rendered
      }
      return keys.map((key, i) => (

          {struct[key].label}

          <Input
            name={key}
            struct={struct[key]}
            value={this.state.struct[key]}
            onChange={this.updateStateKey}
            optionTypes={this.props.optionTypes}
          />

      ))
    }
  }

  renderCollection(struct, key) {
    const { updateRef, optionTypes, collectionTypes } = this.props
    if (struct.type === 'Collection') {
      return this.state.struct[key].map((value, i) => (
        <Struct
          key={i}
          attr={key}
          data={value}
          struct={struct}
          updateRef={updateRef}
          optionTypes={optionTypes}
          collectionTypes={collectionTypes}
          removeFromCollection={this.removeFromCollection}
          updateData={this.updateCollection.bind(this, key)}
        />
      ))
    } else if (struct.type.indexOf('Collection') > -1) {
      const Comp = collectionTypes[struct.type.replace('Collection', '')]
      if (!isUndefined(Comp)) {
        return this.state.struct[key].map((value, i) => (
          <Comp
            key={i}
            attr={key}
            data={value}
            struct={struct}
            optionTypes={optionTypes}
            collectionTypes={collectionTypes}
            removeFromCollection={this.removeFromCollection}
            updateData={this.updateCollection.bind(this, key)}
          />
        ))
      }
    } else if (struct.type === 'KeyValue' || struct.type === 'Object') {
      return (
        <ObjectEditor
          name={key}
          struct={struct}
          optionTypes={optionTypes}
          value={this.state.struct[key]}
          onChange={this.updateStateKey}
          collectionTypes={collectionTypes}
          updateCollectionOrder={this.updateCollectionOrder}
        />
      )
    } else if (struct.type === 'Array') {
      return this.state.struct[key].map((val, i) => (
        <Panel
          key={i}
          header={`${i + 1} - ${val}`}
        >

            {struct.struct.label}

            <Input
              name={i}
              value={val}
              struct={struct.struct}
              onChange={this.updateStateIndex.bind(this, key)}
              optionTypes={this.props.optionTypes}
            />

          <button onClick={this.removeFromCollection.bind(this, key, i)}>
            Remove {pluralize(struct.struct.label, 1)}
          </button>
        </Panel>
      ))
    }
  }

  renderCollections({ struct }) {
    if (!isUndefined(struct)) {
      const isObject = (key) => (CollectionTypes.indexOf(struct[key].type) > -1)
      let keys = Object.keys(struct)
      const isCollection = (key) => (
        struct[key].type.indexOf('Collection') > -1 || isObject(key)
      )
      keys = keys.filter(isCollection)
      if (keys.length > 0) {
        return (
          <Accordion>
            {
              keys.map((key, i) => {
                const data = this.state.struct[key]
                let len = '-1'
                if (data) {
                  len = isObject(key) ? Object.keys(data).length : data.length
                }
                return (
                  <Panel
                    key={i}
                    header={`${struct[key].label} (${len})`}
                  >
                    <Accordion
                      orderable={has(struct[key], 'orderable') ? struct[key].orderable : true}
                      reorder={this.updateCollectionOrder.bind(this, key)}
                    >
                      {this.renderCollection(struct[key], key)}
                    </Accordion>
                    {
                      struct[key].type !== 'Object' &&
                      (
                        <button onClick={this.addToCollection.bind(this, key)}>
                          Add a {pluralize(struct[key].label, 1)}
                        </button>
                      )
                    }
                  </Panel>
                )
              })
            }
          </Accordion>
        )
      }
    }
  }

  render() {
    const { struct } = this.props
    let header = 'No header attr update struct.'
    if (has(struct, 'header')) {
      if (isString(struct.header) && has(this.state.struct, struct.header)) {
        header = this.state.struct[struct.header]
      } else if (isFunction(struct.header)) {
        header = struct.header(this.state.struct, parseInt(this.props.index, 10))
      }
    }
    if (isUndefined(struct.label)) {
      warning('undefined label?', struct)
    }
    if (struct.type === 'Object') {
      return (

          {this.renderNonCollections(struct || {})}
          {this.renderCollections(struct || {})}

          <button onClick={this.updateState}>
            Update {pluralize(struct.label, 1)}
          </button>

      )
    }
    return (
      <Panel header={header} {...this.props}>
        {this.renderNonCollections(struct || {})}
        {this.renderCollections(struct || {})}

        <button onClick={this.updateState}>
          Update {pluralize(struct.label, 1)}
        </button>
        <button onClick={this.handleRemoveClick}>
          Remove {pluralize(struct.label, 1)}
        </button>
      </Panel>
    )
  }
}

Struct.propTypes = {
  data: PropTypes.any,
  attr: PropTypes.any,
  struct: PropTypes.any,
  updateRef: PropTypes.func,
  updateData: PropTypes.func,
  optionTypes: PropTypes.object,
  collectionTypes: PropTypes.object,
  removeFromCollection: PropTypes.func,

  index: PropTypes.any,
  isOpen: PropTypes.any,
  openPanel: PropTypes.func
}

export default Struct
<|endoftext|>


<commit_before>/*
 * Copyright 2015-present Boundless Spatial Inc., http://boundlessgeo.com
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations
 * under the License.
 */

import fetch from 'isomorphic-fetch';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import OlRender from 'ol/render';
import LineString from 'ol/geom/linestring';
import Polygon from 'ol/geom/polygon';
import Point from 'ol/geom/point';
import Feature from 'ol/feature';
import VectorLayer from 'ol/layer/vector';
import { applyStyle } from 'ol-mapbox-style';
import { jsonClone, getLayerById, parseQueryString, encodeQueryObject } from '../util';
import { getFakeStyle, hydrateLayer } from './map';

/** @module components/legend
 * @desc React Component to render the legend data.
 * Create legend objects using the metadata prefixes
 *  "bnd:legend-type" and "bnd:legend-contents".
 *  Neither the Mapbox GL Spec nor the specific underlying
 *  services for vector layers have a standardized way of
 *  providing legends.  This is using the metadata provided
 *  by the layer to do so.
 *  "bnd:legend-type" can be one of "image", "html", "href"
 *  where "bnd:legend-content" would then provide the appropriate
 *  additional information.
 *  "bnd:legend-type" : "image", "bnd:legend-content" would provide
 *   the src attribute for an
 *  "bnd:legend-type" : "html", "bnd:legend-content" would provide
 *   the html content for a

 *  "bnd:legend-type" : "href", "bnd:legend-content" would provide
 *   the URL for html content.
 */

/** Return a div that is asynchronously populated
 *  with the content from the parameter href.
 *
 *  @param {string} href The location of the content for the div.
 *
 *  @returns {Object} A
 element.
 */
function getRemoteLegend(href) {
  let ref = null;
  const div = (
 { ref = me; }} />);

  // kick off the href fun!
  fetch(href).then(response => response.text())
  .then((html) => {
    // This is equivalent to dangerouslySetInnerHTML
    if (ref !== null) {
      ref.innerHTML = html;
    }
  });

  return div;
}

/**
 *   @param {Object} layer Mapbox GL layer.
 *
 *   @returns {(Object|null)} A
 or  element, or null.
 */
export function getLegend(layer) {
  if (layer.metadata === undefined) {
    return null;
  }
  const content = layer.metadata['bnd:legend-content'];

  switch (layer.metadata['bnd:legend-type']) {
    case 'image':
      return ();
    case 'html':
      // eslint-disable-next-line
      return (
);
    case 'href':
      return getRemoteLegend(content);
    default:
      // no legend provided.
      return null;
  }
}

const olLayer = new VectorLayer();

const pointGeomCache = {};
export function getPointGeometry(size) {
  if (!pointGeomCache[size]) {
    pointGeomCache[size] = new Point([size[0] / 2, size[1] / 2]);
  }
  return pointGeomCache[size];
}

const lineGeomCache = {};
export function getLineGeometry(size) {
  if (!lineGeomCache[size]) {
    const center = [size[0] / 2, size[1] / 2];
    lineGeomCache[size] = new LineString([
      [-8 + center[0], -3 + center[1]],
      [-3 + center[0] , 3 + center[1]],
      [3 + center[0], -3 + center[1]],
      [8 + center[0], 3 + center[1]]
    ]);
  }
  return lineGeomCache[size];
}

const polygonGeomCache = {};
export function getPolygonGeometry(size) {
  if (!polygonGeomCache[size]) {
    const center = [size[0] / 2, size[1] / 2];
    polygonGeomCache[size] = new Polygon([[
      [-8 + center[0], -4 + center[1]],
      [-6 + center[0], -6 + center[1]],
      [6 + center[0], -6 + center[1]],
      [8 + center[0], -4 + center[1]],
      [8 + center[0], 4 + center[1]],
      [6 + center[0], 6 + center[1]],
      [-6 + center[0], 6 + center[1]],
      [-8 + center[0], 4 + center[1]]
    ]]);
  }
  return polygonGeomCache[size];
}

export function getVectorLegend(layer, layer_src, props) {
  if (!layer.metadata || !layer.metadata['bnd:legend-type']) {
    const size = props.size;
    return (<canvas ref={(c) => {
      if (c !== null) {
        let vectorContext = OlRender.toContext(c.getContext('2d'), {size: size});
        let newLayer;
        if (layer.filter) {
          newLayer = jsonClone(layer);
          delete newLayer.filter;
        } else {
          newLayer = layer;
        }
        const fake_style = getFakeStyle(
          props.sprite,
          [newLayer],
          props.mapbox.baseUrl,
          props.mapbox.accessToken
        );

        applyStyle(olLayer, fake_style, layer.source).then(function() {
          const styleFn = olLayer.getStyle();
          let geom;
          if (layer.type === 'symbol' || layer.type === 'circle') {
            geom = getPointGeometry(size);
          } else if (layer.type === 'line') {
            geom = getLineGeometry(size);
          } else if (layer.type === 'fill') {
            geom = getPolygonGeometry(size);
          }
          if (geom) {
            const properties = {};
            if (layer['source-layer']) {
              properties.layer = layer['source-layer'];
            }
            const feature = new Feature(properties);
            feature.setGeometry(geom);
            const styles = styleFn(feature);
            for (let i = 0, ii = styles.length; i < ii; ++i) {
              vectorContext.setStyle(styles[i]);
              vectorContext.drawGeometry(geom);
            }
          }
        });
      }
    }} />);
  } else {
    return getLegend(layer, layer_src);
  }
}

/** Get the legend for a raster-type layer.
 *  Attempts to detect a WMS-type source and use GetLegendGraphic,
 *  otherwise, uses SDK specified legend metadata.
 *
 *  @param {Object} layer Mapbox GL layer object.
 *  @param {Object} layer_src Mapbox GL source object.
 *
 *  @returns {(Object[]|Object)} An array of  elements or a
 element.
 */
export function getRasterLegend(layer, layer_src) {
  if (layer_src.tiles && layer_src.tiles.length > 0) {
    const tile_url = layer_src.tiles[0];
    // check to see if the url is a wms request.
    if (tile_url.toUpperCase().indexOf('SERVICE=WMS') >= 0) {
      const tile_url_parts = tile_url.split('?');
      // parse the url
      const wms_params = parseQueryString(tile_url_parts[1]);

      // normalize the keys: WMS requests are sometimes allcaps,
      //  sometimes lower cased, and sometimes (evilly so) mixed case.
      const wms_keys = Object.keys(wms_params);
      for (let i = 0, ii = wms_keys.length; i < ii; i++) {
        const key = wms_keys[i];
        const uc_key = key.toUpperCase();
        wms_params[uc_key] = wms_params[key];
      }

      // get the WMS servers URL.
      const url = tile_url_parts[0];

      // REQUEST, FORMAT, and LAYER are the three required GetLegendGraphic
      // parameters.  LAYER is populated after the optional keys are added.
      const legend_params = {
        SERVICE: 'WMS',
        REQUEST: 'GetLegendGraphic',
        FORMAT: wms_params.FORMAT,
      };

      // These are optional parameters and will not be found in
      // every WMS request. This checks for the parameter before
      // adding it in.
      // WIDTH and HEIGHT are omitted as they provide a
      // hint for the LEGEND size not the underlaying map size.
      const optional_keys = [
        'STYLE', 'FEATURETYPE', 'RULE', 'SCALE',
        'SLD', 'SLD_BODY',
        'EXCEPTIONS', 'LANGUAGE',
      ];

      for (let i = 0, ii = optional_keys.length; i < ii; i++) {
        const value = wms_params[optional_keys[i]];
        if (value !== undefined) {
          legend_params[optional_keys[i]] = value;
        }
      }

      // Build the stack of URLs for each layer. Unlike GetMap,
      // each layer needs a separate call.
      const images = [];
      const layers = wms_params.LAYERS.split(',');
      for (let i = 0, ii = layers.length; i < ii; i++) {
        const params = Object.assign({}, legend_params, {
          LAYER: layers[i],
        });
        const src = `${url}?${encodeQueryObject(params)}`;
        images.push(());
      }

      return images;
    }
  }

  return getLegend(layer);
}

class Legend extends React.Component {

  componentWillReceiveProps(nextProps) {
    const nextLayer = getLayerById(nextProps.layers, this.props.layerId);
    const layer = getLayerById(this.props.layers, this.props.layerId);
    return (layer !== nextLayer);
  }

  /** Handles how to get the legend data based on the layer source type.
   *  @returns {Object} Call to getRasterLegend() or getLegend() to return the html element.
   */
  getLegendContents() {
    // get the layer definition
    const layer = getLayerById(this.props.layers, this.props.layerId);
    if (layer === null) {
      return null;
    }

    let source_name = layer.source;
    if (layer.ref && !layer.source) {
      const ref_layer = getLayerById(this.props.layers, layer.ref);
      source_name = ref_layer.source;
    }
    const layer_src = this.props.sources[source_name];

    switch (layer_src.type) {
      case 'raster':
        return getRasterLegend(layer, layer_src);
      // while this may seem pretty verbose,
      //  it was intentionally left here to make it
      //  easy to implement other legend handlers as
      //  is deemed appropriate.
      case 'vector':
      case 'geojson':
        let legendLayer;
        if (layer.ref) {
          legendLayer = hydrateLayer(this.props.layers, layer);
        } else {
          legendLayer = layer;
        }
        return getVectorLegend(legendLayer, layer_src, this.props);
      case 'image':
      case 'video':
      case 'canvas':
      default:
        return getLegend(layer, layer_src);
    }
  }

  render() {
    let legend_contents = this.getLegendContents();
    if (legend_contents === null) {
      legend_contents = this.props.emptyLegendMessage;
    }
    let className = 'sdk-legend';
    if (this.props.className) {
      className = `${className} ${this.props.className}`;
    }

    return (

        { legend_contents }

    );
  }
}

Legend.propTypes = {
  layerId: PropTypes.string.isRequired,
  layers: PropTypes.arrayOf(PropTypes.object),
  sources: PropTypes.shape({
    source: PropTypes.string,
  }),
  mapbox: PropTypes.shape({
    baseUrl: PropTypes.string,
    accessToken: PropTypes.string,
  }),
  sprite: PropTypes.string,
  emptyLegendMessage: PropTypes.string,
  size: PropTypes.arrayOf(PropTypes.number),
  style: PropTypes.object,
  className: PropTypes.string,
};

Legend.defaultProps = {
  size: [50, 50],
  mapbox: {
    baseUrl: '',
    accessToken: '',
  },
  layers: [],
  sources: {},
  emptyLegendMessage: undefined,
};

function mapStateToProps(state) {
  return {
    sprite: state.map.sprite,
    layers: state.map.layers,
    sources: state.map.sources,
    mapbox: state.mapbox,
  };
}

export default connect(mapStateToProps, undefined, undefined, {withRef: true})(Legend);
<commit_msg>Protect against style function not returning array<commit_after>/*
 * Copyright 2015-present Boundless Spatial Inc., http://boundlessgeo.com
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations
 * under the License.
 */

import fetch from 'isomorphic-fetch';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import OlRender from 'ol/render';
import LineString from 'ol/geom/linestring';
import Polygon from 'ol/geom/polygon';
import Point from 'ol/geom/point';
import Feature from 'ol/feature';
import VectorLayer from 'ol/layer/vector';
import { applyStyle } from 'ol-mapbox-style';
import { jsonClone, getLayerById, parseQueryString, encodeQueryObject } from '../util';
import { getFakeStyle, hydrateLayer } from './map';

/** @module components/legend
 * @desc React Component to render the legend data.
 * Create legend objects using the metadata prefixes
 *  "bnd:legend-type" and "bnd:legend-contents".
 *  Neither the Mapbox GL Spec nor the specific underlying
 *  services for vector layers have a standardized way of
 *  providing legends.  This is using the metadata provided
 *  by the layer to do so.
 *  "bnd:legend-type" can be one of "image", "html", "href"
 *  where "bnd:legend-content" would then provide the appropriate
 *  additional information.
 *  "bnd:legend-type" : "image", "bnd:legend-content" would provide
 *   the src attribute for an
 *  "bnd:legend-type" : "html", "bnd:legend-content" would provide
 *   the html content for a

 *  "bnd:legend-type" : "href", "bnd:legend-content" would provide
 *   the URL for html content.
 */

/** Return a div that is asynchronously populated
 *  with the content from the parameter href.
 *
 *  @param {string} href The location of the content for the div.
 *
 *  @returns {Object} A
 element.
 */
function getRemoteLegend(href) {
  let ref = null;
  const div = (
 { ref = me; }} />);

  // kick off the href fun!
  fetch(href).then(response => response.text())
  .then((html) => {
    // This is equivalent to dangerouslySetInnerHTML
    if (ref !== null) {
      ref.innerHTML = html;
    }
  });

  return div;
}

/**
 *   @param {Object} layer Mapbox GL layer.
 *
 *   @returns {(Object|null)} A
 or  element, or null.
 */
export function getLegend(layer) {
  if (layer.metadata === undefined) {
    return null;
  }
  const content = layer.metadata['bnd:legend-content'];

  switch (layer.metadata['bnd:legend-type']) {
    case 'image':
      return ();
    case 'html':
      // eslint-disable-next-line
      return (
);
    case 'href':
      return getRemoteLegend(content);
    default:
      // no legend provided.
      return null;
  }
}

const olLayer = new VectorLayer();

const pointGeomCache = {};
export function getPointGeometry(size) {
  if (!pointGeomCache[size]) {
    pointGeomCache[size] = new Point([size[0] / 2, size[1] / 2]);
  }
  return pointGeomCache[size];
}

const lineGeomCache = {};
export function getLineGeometry(size) {
  if (!lineGeomCache[size]) {
    const center = [size[0] / 2, size[1] / 2];
    lineGeomCache[size] = new LineString([
      [-8 + center[0], -3 + center[1]],
      [-3 + center[0] , 3 + center[1]],
      [3 + center[0], -3 + center[1]],
      [8 + center[0], 3 + center[1]]
    ]);
  }
  return lineGeomCache[size];
}

const polygonGeomCache = {};
export function getPolygonGeometry(size) {
  if (!polygonGeomCache[size]) {
    const center = [size[0] / 2, size[1] / 2];
    polygonGeomCache[size] = new Polygon([[
      [-8 + center[0], -4 + center[1]],
      [-6 + center[0], -6 + center[1]],
      [6 + center[0], -6 + center[1]],
      [8 + center[0], -4 + center[1]],
      [8 + center[0], 4 + center[1]],
      [6 + center[0], 6 + center[1]],
      [-6 + center[0], 6 + center[1]],
      [-8 + center[0], 4 + center[1]]
    ]]);
  }
  return polygonGeomCache[size];
}

export function getVectorLegend(layer, layer_src, props) {
  if (!layer.metadata || !layer.metadata['bnd:legend-type']) {
    const size = props.size;
    return (<canvas ref={(c) => {
      if (c !== null) {
        let vectorContext = OlRender.toContext(c.getContext('2d'), {size: size});
        let newLayer;
        if (layer.filter) {
          newLayer = jsonClone(layer);
          delete newLayer.filter;
        } else {
          newLayer = layer;
        }
        const fake_style = getFakeStyle(
          props.sprite,
          [newLayer],
          props.mapbox.baseUrl,
          props.mapbox.accessToken
        );

        applyStyle(olLayer, fake_style, layer.source).then(function() {
          const styleFn = olLayer.getStyle();
          let geom;
          if (layer.type === 'symbol' || layer.type === 'circle') {
            geom = getPointGeometry(size);
          } else if (layer.type === 'line') {
            geom = getLineGeometry(size);
          } else if (layer.type === 'fill') {
            geom = getPolygonGeometry(size);
          }
          if (geom) {
            const properties = {};
            if (layer['source-layer']) {
              properties.layer = layer['source-layer'];
            }
            const feature = new Feature(properties);
            feature.setGeometry(geom);
            const styles = styleFn(feature);
            if (styles) {
              for (let i = 0, ii = styles.length; i < ii; ++i) {
                vectorContext.setStyle(styles[i]);
                vectorContext.drawGeometry(geom);
              }
            }
          }
        });
      }
    }} />);
  } else {
    return getLegend(layer, layer_src);
  }
}

/** Get the legend for a raster-type layer.
 *  Attempts to detect a WMS-type source and use GetLegendGraphic,
 *  otherwise, uses SDK specified legend metadata.
 *
 *  @param {Object} layer Mapbox GL layer object.
 *  @param {Object} layer_src Mapbox GL source object.
 *
 *  @returns {(Object[]|Object)} An array of  elements or a
 element.
 */
export function getRasterLegend(layer, layer_src) {
  if (layer_src.tiles && layer_src.tiles.length > 0) {
    const tile_url = layer_src.tiles[0];
    // check to see if the url is a wms request.
    if (tile_url.toUpperCase().indexOf('SERVICE=WMS') >= 0) {
      const tile_url_parts = tile_url.split('?');
      // parse the url
      const wms_params = parseQueryString(tile_url_parts[1]);

      // normalize the keys: WMS requests are sometimes allcaps,
      //  sometimes lower cased, and sometimes (evilly so) mixed case.
      const wms_keys = Object.keys(wms_params);
      for (let i = 0, ii = wms_keys.length; i < ii; i++) {
        const key = wms_keys[i];
        const uc_key = key.toUpperCase();
        wms_params[uc_key] = wms_params[key];
      }

      // get the WMS servers URL.
      const url = tile_url_parts[0];

      // REQUEST, FORMAT, and LAYER are the three required GetLegendGraphic
      // parameters.  LAYER is populated after the optional keys are added.
      const legend_params = {
        SERVICE: 'WMS',
        REQUEST: 'GetLegendGraphic',
        FORMAT: wms_params.FORMAT,
      };

      // These are optional parameters and will not be found in
      // every WMS request. This checks for the parameter before
      // adding it in.
      // WIDTH and HEIGHT are omitted as they provide a
      // hint for the LEGEND size not the underlaying map size.
      const optional_keys = [
        'STYLE', 'FEATURETYPE', 'RULE', 'SCALE',
        'SLD', 'SLD_BODY',
        'EXCEPTIONS', 'LANGUAGE',
      ];

      for (let i = 0, ii = optional_keys.length; i < ii; i++) {
        const value = wms_params[optional_keys[i]];
        if (value !== undefined) {
          legend_params[optional_keys[i]] = value;
        }
      }

      // Build the stack of URLs for each layer. Unlike GetMap,
      // each layer needs a separate call.
      const images = [];
      const layers = wms_params.LAYERS.split(',');
      for (let i = 0, ii = layers.length; i < ii; i++) {
        const params = Object.assign({}, legend_params, {
          LAYER: layers[i],
        });
        const src = `${url}?${encodeQueryObject(params)}`;
        images.push(());
      }

      return images;
    }
  }

  return getLegend(layer);
}

class Legend extends React.Component {

  componentWillReceiveProps(nextProps) {
    const nextLayer = getLayerById(nextProps.layers, this.props.layerId);
    const layer = getLayerById(this.props.layers, this.props.layerId);
    return (layer !== nextLayer);
  }

  /** Handles how to get the legend data based on the layer source type.
   *  @returns {Object} Call to getRasterLegend() or getLegend() to return the html element.
   */
  getLegendContents() {
    // get the layer definition
    const layer = getLayerById(this.props.layers, this.props.layerId);
    if (layer === null) {
      return null;
    }

    let source_name = layer.source;
    if (layer.ref && !layer.source) {
      const ref_layer = getLayerById(this.props.layers, layer.ref);
      source_name = ref_layer.source;
    }
    const layer_src = this.props.sources[source_name];

    switch (layer_src.type) {
      case 'raster':
        return getRasterLegend(layer, layer_src);
      // while this may seem pretty verbose,
      //  it was intentionally left here to make it
      //  easy to implement other legend handlers as
      //  is deemed appropriate.
      case 'vector':
      case 'geojson':
        let legendLayer;
        if (layer.ref) {
          legendLayer = hydrateLayer(this.props.layers, layer);
        } else {
          legendLayer = layer;
        }
        return getVectorLegend(legendLayer, layer_src, this.props);
      case 'image':
      case 'video':
      case 'canvas':
      default:
        return getLegend(layer, layer_src);
    }
  }

  render() {
    let legend_contents = this.getLegendContents();
    if (legend_contents === null) {
      legend_contents = this.props.emptyLegendMessage;
    }
    let className = 'sdk-legend';
    if (this.props.className) {
      className = `${className} ${this.props.className}`;
    }

    return (

        { legend_contents }

    );
  }
}

Legend.propTypes = {
  layerId: PropTypes.string.isRequired,
  layers: PropTypes.arrayOf(PropTypes.object),
  sources: PropTypes.shape({
    source: PropTypes.string,
  }),
  mapbox: PropTypes.shape({
    baseUrl: PropTypes.string,
    accessToken: PropTypes.string,
  }),
  sprite: PropTypes.string,
  emptyLegendMessage: PropTypes.string,
  size: PropTypes.arrayOf(PropTypes.number),
  style: PropTypes.object,
  className: PropTypes.string,
};

Legend.defaultProps = {
  size: [50, 50],
  mapbox: {
    baseUrl: '',
    accessToken: '',
  },
  layers: [],
  sources: {},
  emptyLegendMessage: undefined,
};

function mapStateToProps(state) {
  return {
    sprite: state.map.sprite,
    layers: state.map.layers,
    sources: state.map.sources,
    mapbox: state.mapbox,
  };
}

export default connect(mapStateToProps, undefined, undefined, {withRef: true})(Legend);
<|endoftext|>


<commit_before>import React from 'react';
import Rebase from 're-base';

import InlineSVG from 'svg-inline-react';
import svg from '!svg-inline!../assets/svg/radio-station.svg';

import MusicStream from '../classes/musicStream';

export default class Viewer extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      display: {},
      classId: this.generateId(),
    };

    this.matrixText = '';
  }

  componentWillMount() {
    this.base = Rebase.createClass('https://blink-ct.firebaseio.com/classes/' + this.state.classId);

    this.ref = this.base.syncState('display', {
      context: this,
      state: 'display'
    });

    this.musicStream = new MusicStream();
  }

  componentDidMount(){
    this.refs.chat.scrollTop = this.refs.chat.scrollHeight;

    this.matrixInterval = setInterval(() => {
      this.theMatrix();
    }, 100);
  }

  theMatrix(){
    const matrixLength = 225;

    let matrix = this.matrixText + '' + Math.round(Math.random());
    if(matrix.length > matrixLength){
      matrix = matrix.substring(matrix.length - matrixLength)
    }

    this.matrixText = matrix;
    this.refs.matrix.innerHTML = matrix;
  }

  componentDidUpdate(){
    this.refs.chat.scrollTop = this.refs.chat.scrollHeight;

    // TODO: Check if music playing user diconnected.

    const stream = this.getStream();
    if(this.currentStream === stream) {
      return;
    }

    this.currentStream = stream;
    this.musicStream.play(stream);
  }

  getStream() {
    return (this.state.display.music || {}).value || 'uit';
  }

  componentWillUnmount(){
    this.base.removeBinding(this.ref);
    clearInterval(this.matrixInterval);
  }

  generateId() {
    const num = Math.floor(Math.random() * 1000000).toString();
    const pad = '000000';
    return pad.slice(num.length) + num;
  }

  render() {
    const sessions = this.state.display.sessions || {};
    const stream = this.getStream();
    const streamSessionKey = (this.state.display.music || {}).sessionKey;
    const session = sessions[streamSessionKey] || {};
    const userName = session.name || 'Anoniempje';
    const playing = stream !== 'uit' ?

Je luistert nu naar: { stream }

de keuze van { userName }

 :

Geen muziek geselecteerd

;

    const nrSessions = this.state.display.sessions ? Object.keys(sessions).length : 0;

    return(

            Digibord ID: { this.state.classId }

            { this.renderMessages() }

{ nrSessions } gebruiker{ nrSessions !== 1 ? 's' : '' }

              <InlineSVG src={ svg } />

            { playing }

          { this.renderLights() }

    );
  }

  renderMessages() {
    const messages = this.state.display.messages || {};
    const sessions = this.state.display.sessions || {};
    return Object.entries(messages).map(([key, message]) => {
      const session = sessions[message.sessionKey] || {};
      const userName = session.name || 'Anoniempje';

      return (

{ userName } zegt:

{ message.message }

      );
    });
  }

  renderLights() {
    const sessions = this.state.display.sessions || {};

    return Object.entries(sessions).map(([key, session]) => {
      let style = {};
      let borderColor = {};
      let beamClass = 'beam';

      if(typeof session.light === 'object'){
        style = {
          backgroundColor: 'rgb(' + session.light.r + ',' + session.light.g + ',' + session.light.b + ')'
        };
        borderColor = {
          borderColor: 'rgba(' + session.light.r + ',' + session.light.g + ',' + session.light.b + ',0.8)'
        }
      }
      else if(session.light === 'uit') {
        beamClass += ' light-off';
        borderColor = {
          borderColor: 'rgba(180,180,180,0.5)'
        }
      }

      const userName = session.name || '';
      const matches = userName.match(/\b(\w)/g);
      const acronym = (matches || []).slice(0,2).join('');

      return (

{ acronym }

      );
    });
  }
}
<commit_msg>Sort methods.<commit_after>import React from 'react';
import Rebase from 're-base';

import InlineSVG from 'svg-inline-react';
import svg from '!svg-inline!../assets/svg/radio-station.svg';

import MusicStream from '../classes/musicStream';

export default class Viewer extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      display: {},
      classId: this.generateId(),
    };

    this.matrixText = '';
  }

  componentWillMount() {
    this.base = Rebase.createClass('https://blink-ct.firebaseio.com/classes/' + this.state.classId);

    this.ref = this.base.syncState('display', {
      context: this,
      state: 'display'
    });

    this.musicStream = new MusicStream();
  }

  componentDidMount(){
    this.refs.chat.scrollTop = this.refs.chat.scrollHeight;

    this.matrixInterval = setInterval(() => {
      this.theMatrix();
    }, 100);
  }

  componentDidUpdate(){
    this.refs.chat.scrollTop = this.refs.chat.scrollHeight;

    // TODO: Check if music playing user diconnected.

    const stream = this.getStream();
    if(this.currentStream === stream) {
      return;
    }

    this.currentStream = stream;
    this.musicStream.play(stream);
  }

  componentWillUnmount() {
    this.base.removeBinding(this.ref);
    clearInterval(this.matrixInterval);
  }

  generateId() {
    const num = Math.floor(Math.random() * 1000000).toString();
    const pad = '000000';
    return pad.slice(num.length) + num;
  }

  theMatrix() {
    const matrixLength = 225;

    let matrix = this.matrixText + '' + Math.round(Math.random());
    if(matrix.length > matrixLength){
      matrix = matrix.substring(matrix.length - matrixLength)
    }

    this.matrixText = matrix;
    this.refs.matrix.innerHTML = matrix;
  }

  getStream() {
    return (this.state.display.music || {}).value || 'uit';
  }

  render() {
    const sessions = this.state.display.sessions || {};
    const stream = this.getStream();
    const streamSessionKey = (this.state.display.music || {}).sessionKey;
    const session = sessions[streamSessionKey] || {};
    const userName = session.name || 'Anoniempje';
    const playing = stream !== 'uit' ?

Je luistert nu naar: { stream }

de keuze van { userName }

 :

Geen muziek geselecteerd

;

    const nrSessions = this.state.display.sessions ? Object.keys(sessions).length : 0;

    return(

            Digibord ID: { this.state.classId }

            { this.renderMessages() }

{ nrSessions } gebruiker{ nrSessions !== 1 ? 's' : '' }

              <InlineSVG src={ svg } />

            { playing }

          { this.renderLights() }

    );
  }

  renderMessages() {
    const messages = this.state.display.messages || {};
    const sessions = this.state.display.sessions || {};
    return Object.entries(messages).map(([key, message]) => {
      const session = sessions[message.sessionKey] || {};
      const userName = session.name || 'Anoniempje';

      return (

{ userName } zegt:

{ message.message }

      );
    });
  }

  renderLights() {
    const sessions = this.state.display.sessions || {};

    return Object.entries(sessions).map(([key, session]) => {
      let style = {};
      let borderColor = {};
      let beamClass = 'beam';

      if(typeof session.light === 'object'){
        style = {
          backgroundColor: 'rgb(' + session.light.r + ',' + session.light.g + ',' + session.light.b + ')'
        };
        borderColor = {
          borderColor: 'rgba(' + session.light.r + ',' + session.light.g + ',' + session.light.b + ',0.8)'
        }
      }
      else if(session.light === 'uit') {
        beamClass += ' light-off';
        borderColor = {
          borderColor: 'rgba(180,180,180,0.5)'
        }
      }

      const userName = session.name || '';
      const matches = userName.match(/\b(\w)/g);
      const acronym = (matches || []).slice(0,2).join('');

      return (

{ acronym }

      );
    });
  }
}
<|endoftext|>