<commit_before>import React from 'react';
import Dropzone from 'react-dropzone';

import sorter from './../Player/utils/sort';
import parser from './../Player/utils/parser';

import MainMenuActions from './actions';
import PlayerActions from '../../components/Player/actions';
import ModalActions from './../Modal/actions';
import MessageActions from '../Message/actions';
import TorrentActions from '../../actions/torrentActions';
import metaParser from '../../components/Player/utils/metaParser';
import Plugins from './components/Plugins';
import Settings from './components/Settings';
import remote from 'remote';
import path from 'path';

import {
    webFrame
}
from 'electron';

import linkUtil from '../../utils/linkUtil';

import _ from 'lodash';

export
default React.createClass({

    getInitialState() {
        return {
            dropBorderColor: '#ccc',
            lastZoom: 0,
            extensionView: false,
            settingsView: false
        }
    },
    componentWillMount() {
        window.addEventListener('resize', this.handleResize);
        var currentWindow = remote.getCurrentWindow();
        currentWindow.setMinimumSize(410, 324);
        var currentSize = currentWindow.getSize();
        if (currentSize[0] < 410) currentSize[0] = 410;
        if (currentSize[1] < 324) currentSize[1] = 324;
        currentWindow.setSize(currentSize[0], currentSize[1]);
        this.handleResize();
    },

    componentDidMount() {

    },

    componentWillUnmount() {
        window.removeEventListener('resize', this.handleResize);
    },

    handleResize() {

        var width = remote.getCurrentWindow().getSize()[0];
        var newZoom = 0;

        if (width < 407)
            newZoom = -2.3;
        else if (width >= 407 && width < 632)
            newZoom = (2.3 - ((width - 407) / 97.8)) * (-1);
        else if (width >= 632 && width < 755)
            newZoom = (width - 632) / 123;
        else
            newZoom = 1;

        if (newZoom != this.state.lastZoom) {
            this.setState({
                lastZoom: newZoom
            });
            webFrame.setZoomLevel(newZoom);
        }

    },

    onDrop(files,e) {
        if (files && files.length) {

            var ext = path.extname(files[0].path);

            if (['.torrent', '.magnet'].indexOf(ext) > -1) {

                ModalActions.open({
                    type: 'thinking'
                });

                TorrentActions.addTorrent(files[0].path);

            } else {

                var newFiles = [];
                var queueParser = [];

                if (parser(files[0].name).shortSzEp())
                    files = sorter.episodes(files, 2);
                else
                    files = sorter.naturalSort(files, 2);

                files.forEach( (file, ij) => {
                    newFiles.push({
                        title: parser(file.name).name(),
                        uri: 'file:///'+file.path,
                        path: file.path
                    });
                    queueParser.push({
                        idx: ij,
                        url: 'file:///'+file.path,
                        filename: file.name
                    });
                });

                PlayerActions.addPlaylist(newFiles);

                // start searching for thumbnails after 1 second
                _.delay(() => {
                    queueParser.forEach( el => {
                        metaParser.push(el);
                    });
                },1000);
            }
        } else {
            var droppedLink = e.dataTransfer.getData("text/plain");
            if (droppedLink) {

                ModalActions.open({
                    title: 'Thinking',
                    type: 'thinking'
                });

                linkUtil(droppedLink).then(url => {
                    ModalActions.thinking(false);
                }).catch(error => {
                    ModalActions.thinking(false);
                    MessageActions.open(error.message);
                });

            }
        }
        document.querySelector('.wrapper .holder').classList.remove('holder-hover');
    },
    onDragEnter() {
        document.querySelector('.wrapper .holder').classList.add('holder-hover');
    },
    onDragLeave() {
        document.querySelector('.wrapper .holder').classList.remove('holder-hover');
    },
    extensionView() {
        var viewHolder = window.document.querySelector(".wrapper");

        if (viewHolder.className.includes('settingsView'))
            viewHolder.className = viewHolder.className.replace(' settingsView', '');

        if (viewHolder.className.includes('extensionView')) {
            viewHolder.className = viewHolder.className.replace(' extensionView', '');
            this.setState({
                settingsView: false,
                extensionView: false
            });
        } else {
            viewHolder.className += ' extensionView';
            this.setState({
                settingsView: false,
                extensionView: true
            });
        }
    },
    settingsView() {
        var viewHolder = window.document.querySelector(".wrapper");

        if (viewHolder.className.includes('extensionView'))
            viewHolder.className = viewHolder.className.replace(' extensionView', '');

        if (viewHolder.className.includes('settingsView')) {
            viewHolder.className = viewHolder.className.replace(' settingsView', '');
            this.setState({
                extensionView: false,
                settingsView: false
            });
        } else {
            viewHolder.className += ' settingsView';
            this.setState({
                extensionView: false,
                settingsView: true
            });
        }
    },
    render() {
        var extensionView = this.state.extensionView ? (<Plugins />) : '';
        var settingsView = this.state.settingsView ? (<Settings />) : '';
        return (

               {extensionView}
               {settingsView}

                    <Dropzone ref="dropper" disableClick={true} className="holder" onDragEnter={this.onDragEnter} onDragLeave={this.onDragLeave} onDrop={this.onDrop} style={{}}>

                                    <paper-icon-button id="main_plugins_but" icon="extension" alt="plugins" style={{color: '#767A7B', width: '44px', height: '44px', right: '2px', position: 'absolute', marginRight: '48px', marginTop: '2px'}} onClick={this.extensionView} />
                                    <paper-tooltip for="main_plugins_but" offset="0">Plugins</paper-tooltip>
                                    <paper-icon-button id="main_settings_but" icon="settings" alt="settings" style={{color: '#767A7B', width: '48px', height: '48px', right: '2px', position: 'absolute'}} onClick={this.settingsView} />
                                    <paper-tooltip for="main_settings_but" offset="0">Settings</paper-tooltip>

                            Drag & Drop a File

                            or select an option below

                                <paper-button raised style={{float: 'left', width: '130px', height: '108px', background: '#00b850'}} onClick={MainMenuActions.openLocal.bind(this, 'torrent')}>

                                    Add Torrent

                                </paper-button>
                                <paper-button raised style={{float: 'left', width: '130px', height: '108px', background: '#1ca8ed', margin: '0 1.2em'}} onClick={MainMenuActions.openLocal.bind(this, 'video')}>

                                    Add Video

                                </paper-button>
                                <paper-button raised style={{float: 'left', width: '130px', height: '108px', background: '#f1664f'}} onClick={MainMenuActions.openURL}>

                                    Use a URL

                                </paper-button>

                    </Dropzone>

        );
    }
});<commit_msg>Drag & Drop Folders in Main Menu<commit_after>import React from 'react';
import Dropzone from 'react-dropzone';

import sorter from './../Player/utils/sort';
import parser from './../Player/utils/parser';

import MainMenuActions from './actions';
import PlayerActions from '../../components/Player/actions';
import ModalActions from './../Modal/actions';
import MessageActions from '../Message/actions';
import TorrentActions from '../../actions/torrentActions';
import metaParser from '../../components/Player/utils/metaParser';
import Plugins from './components/Plugins';
import Settings from './components/Settings';
import remote from 'remote';
import path from 'path';
import fs from 'fs';
import player from '../Player/utils/player';

import {
    webFrame
}
from 'electron';

import linkUtil from '../../utils/linkUtil';

import _ from 'lodash';

export
default React.createClass({

    getInitialState() {
        return {
            dropBorderColor: '#ccc',
            lastZoom: 0,
            extensionView: false,
            settingsView: false
        }
    },
    componentWillMount() {
        window.addEventListener('resize', this.handleResize);
        var currentWindow = remote.getCurrentWindow();
        currentWindow.setMinimumSize(410, 324);
        var currentSize = currentWindow.getSize();
        if (currentSize[0] < 410) currentSize[0] = 410;
        if (currentSize[1] < 324) currentSize[1] = 324;
        currentWindow.setSize(currentSize[0], currentSize[1]);
        this.handleResize();
    },

    componentDidMount() {

    },

    componentWillUnmount() {
        window.removeEventListener('resize', this.handleResize);
    },

    handleResize() {

        var width = remote.getCurrentWindow().getSize()[0];
        var newZoom = 0;

        if (width < 407)
            newZoom = -2.3;
        else if (width >= 407 && width < 632)
            newZoom = (2.3 - ((width - 407) / 97.8)) * (-1);
        else if (width >= 632 && width < 755)
            newZoom = (width - 632) / 123;
        else
            newZoom = 1;

        if (newZoom != this.state.lastZoom) {
            this.setState({
                lastZoom: newZoom
            });
            webFrame.setZoomLevel(newZoom);
        }

    },

    onDrop(files,e) {
        if (files && files.length) {

            var ext = path.extname(files[0].path);

            if (['.torrent', '.magnet'].indexOf(ext) > -1) {

                ModalActions.open({
                    type: 'thinking'
                });

                TorrentActions.addTorrent(files[0].path);

            } else {

                var supported = [".mkv", ".avi", ".mp4", ".m4v", ".mpg", ".mpeg", ".webm", ".flv", ".ogg", ".ogv", ".mov", ".wmv", ".3gp", ".3g2", ".m4a", ".mp3", ".flac"];

                var newFiles = [];
                var queueParser = [];

                if (parser(files[0].name).shortSzEp())
                    files = sorter.episodes(files, 2);
                else
                    files = sorter.naturalSort(files, 2);

                var itemCount = player && player.wcjs ? player.wcjs.playlist.itemCount : 0;

                var idx = itemCount;

                var addFile = (filePath) => {
                    if (isSupported(filePath)) {
                        newFiles.push({
                            title: parser(filePath).name(),
                            uri: 'file:///'+filePath,
                            path: filePath
                        });
                        queueParser.push({
                            idx: idx,
                            url: 'file:///'+filePath,
                            filename: filePath.replace(/^.*[\\\/]/, '')
                        });
                        idx++;
                    }

                    return false;
                };

                var addDir = (filePath) => {
                    var newFiles = fs.readdirSync(filePath);

                    if (parser(newFiles[0]).shortSzEp())
                        newFiles = sorter.episodes(newFiles, 1);
                    else
                        newFiles = sorter.naturalSort(newFiles, 1);

                    newFiles.forEach(( file, index ) => {
                        var dummy = decide( path.join( filePath, file ) );
                    });

                    return false;
                };

                var decide = (filePath) => {
                    if (fs.lstatSync(filePath).isDirectory())
                        var dummy = addDir(filePath);
                    else
                        var dummy = addFile(filePath);

                    return false;
                };

                var isSupported = (filePath) => {
                    return supported.some( el => {
                        if (filePath.endsWith(el)) return true;
                    });
                }

                files.forEach( (file, ij) => {
                    var dummy = decide(file.path);
                });

                PlayerActions.addPlaylist(newFiles);

                // start searching for thumbnails after 1 second
                _.delay(() => {
                    queueParser.forEach( el => {
                        metaParser.push(el);
                    });
                },1000);
            }
        } else {
            var droppedLink = e.dataTransfer.getData("text/plain");
            if (droppedLink) {

                ModalActions.open({
                    title: 'Thinking',
                    type: 'thinking'
                });

                linkUtil(droppedLink).then(url => {
                    ModalActions.thinking(false);
                }).catch(error => {
                    ModalActions.thinking(false);
                    MessageActions.open(error.message);
                });

            }
        }
        document.querySelector('.wrapper .holder').classList.remove('holder-hover');
    },
    onDragEnter() {
        document.querySelector('.wrapper .holder').classList.add('holder-hover');
    },
    onDragLeave() {
        document.querySelector('.wrapper .holder').classList.remove('holder-hover');
    },
    extensionView() {
        var viewHolder = window.document.querySelector(".wrapper");

        if (viewHolder.className.includes('settingsView'))
            viewHolder.className = viewHolder.className.replace(' settingsView', '');

        if (viewHolder.className.includes('extensionView')) {
            viewHolder.className = viewHolder.className.replace(' extensionView', '');
            this.setState({
                settingsView: false,
                extensionView: false
            });
        } else {
            viewHolder.className += ' extensionView';
            this.setState({
                settingsView: false,
                extensionView: true
            });
        }
    },
    settingsView() {
        var viewHolder = window.document.querySelector(".wrapper");

        if (viewHolder.className.includes('extensionView'))
            viewHolder.className = viewHolder.className.replace(' extensionView', '');

        if (viewHolder.className.includes('settingsView')) {
            viewHolder.className = viewHolder.className.replace(' settingsView', '');
            this.setState({
                extensionView: false,
                settingsView: false
            });
        } else {
            viewHolder.className += ' settingsView';
            this.setState({
                extensionView: false,
                settingsView: true
            });
        }
    },
    render() {
        var extensionView = this.state.extensionView ? (<Plugins />) : '';
        var settingsView = this.state.settingsView ? (<Settings />) : '';
        return (

               {extensionView}
               {settingsView}

                    <Dropzone ref="dropper" disableClick={true} className="holder" onDragEnter={this.onDragEnter} onDragLeave={this.onDragLeave} onDrop={this.onDrop} style={{}}>

                                    <paper-icon-button id="main_plugins_but" icon="extension" alt="plugins" style={{color: '#767A7B', width: '44px', height: '44px', right: '2px', position: 'absolute', marginRight: '48px', marginTop: '2px'}} onClick={this.extensionView} />
                                    <paper-tooltip for="main_plugins_but" offset="0">Plugins</paper-tooltip>
                                    <paper-icon-button id="main_settings_but" icon="settings" alt="settings" style={{color: '#767A7B', width: '48px', height: '48px', right: '2px', position: 'absolute'}} onClick={this.settingsView} />
                                    <paper-tooltip for="main_settings_but" offset="0">Settings</paper-tooltip>

                            Drag & Drop a File

                            or select an option below

                                <paper-button raised style={{float: 'left', width: '130px', height: '108px', background: '#00b850'}} onClick={MainMenuActions.openLocal.bind(this, 'torrent')}>

                                    Add Torrent

                                </paper-button>
                                <paper-button raised style={{float: 'left', width: '130px', height: '108px', background: '#1ca8ed', margin: '0 1.2em'}} onClick={MainMenuActions.openLocal.bind(this, 'video')}>

                                    Add Video

                                </paper-button>
                                <paper-button raised style={{float: 'left', width: '130px', height: '108px', background: '#f1664f'}} onClick={MainMenuActions.openURL}>

                                    Use a URL

                                </paper-button>

                    </Dropzone>

        );
    }
});<|endoftext|>


<commit_before>import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import Button from 'react-bootstrap/lib/Button';
import Overlay from 'react-bootstrap/lib/Overlay';
import Popover from 'react-bootstrap/lib/Popover';
import FontAwesomeIcon from 'fm3/components/FontAwesomeIcon';
import tips from 'fm3/tips/index.json';

import { setActiveModal, setLocation } from 'fm3/actions/mainActions';
import { authStartLogout } from 'fm3/actions/authActions';
import { tipsShow } from 'fm3/actions/tipsActions';

class MoreMenuButton extends React.Component {
  static propTypes = {
    onSettingsShow: PropTypes.func.isRequired,
    onGpxExport: PropTypes.func.isRequired,
    onShare: PropTypes.func.isRequired,
    onEmbed: PropTypes.func.isRequired,
    onSupportUs: PropTypes.func.isRequired,
    onAbout: PropTypes.func.isRequired,
    onLegend: PropTypes.func.isRequired,
    onLogin: PropTypes.func.isRequired,
    onLogout: PropTypes.func.isRequired,
    user: PropTypes.shape({
      name: PropTypes.string.isRequired,
    }),
    onTip: PropTypes.func.isRequired,
  };

  state = {
    show: false,
    submenu: null,
  }

  setButton = (button) => {
    this.button = button;
  };

  handleButtonClick = () => {
    this.setState({ show: true });
  }

  handleHide = () => {
    this.close();
  }

  handleItemClick = () => {
    this.close();
  }

  handleLoginClick = () => {
    this.close();
    this.props.onLogin();
  }

  handleLogoutClick = () => {
    this.close();
    this.props.onLogout();
  }

  handleShareClick = () => {
    this.close();
    this.props.onShare();
  }

  handleSettingsShowClick = () => {
    this.close();
    this.props.onSettingsShow();
  }

  handleGpxExportClick = () => {
    this.close();
    this.props.onGpxExport();
  }

  handleEmbedClick = () => {
    this.close();
    this.props.onEmbed();
  }

  handleSupportUsClick = () => {
    this.close();
    this.props.onSupportUs();
  }

  handleAboutClick = () => {
    this.close();
    this.props.onAbout();
  }

  handleLegendClick = () => {
    this.close();
    this.props.onLegend();
  }

  handleHelpClick = () => {
    this.setState({ submenu: 'help' });
  }

  handleBackClick = () => {
    this.setState({ submenu: null });
  }

  close = () => {
    this.setState({
      show: false,
      submenu: null,
    });
  }

  handleTipSelect = (tip) => {
    this.props.onTip(tip);
    this.close();
  }

  render() {
    const { user } = this.props;
    const { submenu } = this.state;

    return (
      <React.Fragment>
        <Button ref={this.setButton} onClick={this.handleButtonClick} title="Ďalšie">
          <FontAwesomeIcon icon="ellipsis-v" />
        </Button>
        <Overlay
          rootClose
          placement="bottom"
          show={this.state.show}
          onHide={this.handleHide}
          target={() => this.button}
          shouldUpdatePosition
        >
          <Popover id="popover-trigger-click-root-close" className="fm-menu">

              {submenu === null ?
                <React.Fragment>
                  {
                    user ?
                      <MenuItem onClick={this.handleLogoutClick}>
                        <FontAwesomeIcon icon="sign-out" /> Odhlásiť {user.name}
                      </MenuItem>
                      :
                      <MenuItem onClick={this.handleLoginClick}>
                        <FontAwesomeIcon icon="sign-in" /> Prihlásenie
                      </MenuItem>
                  }
                  <MenuItem onClick={this.handleSettingsShowClick}>
                    <FontAwesomeIcon icon="cog" /> Nastavenia
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleGpxExportClick}>
                    <FontAwesomeIcon icon="share" /> Exportovať do GPX
                  </MenuItem>
                  <MenuItem onClick={this.handleItemClick} href="http://wiki.freemap.sk/FileDownload" target="_blank">
                    <FontAwesomeIcon icon="mobile" /> Exporty mapy
                  </MenuItem>
                  <MenuItem onClick={this.handleShareClick}>
                    <FontAwesomeIcon icon="share-alt" /> Zdieľať mapu
                  </MenuItem>
                  <MenuItem onClick={this.handleEmbedClick}>
                    <FontAwesomeIcon icon="code" /> Vložiť do webstránky
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleItemClick} href="http://wiki.freemap.sk/NahlasenieChyby" target="_blank">
                    <FontAwesomeIcon icon="exclamation-triangle" /> Nahlásiť chybu zobrazenia v mape
                  </MenuItem>
                  <MenuItem onClick={this.handleItemClick} href="https://github.com/FreemapSlovakia/freemap-v3-react/issues" target="_blank">
                    <FontAwesomeIcon icon="exclamation-triangle" /> Nahlásiť chybu v portáli
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleHelpClick}>
                    <FontAwesomeIcon icon="book" /> Pomoc <FontAwesomeIcon icon="chevron-right" />
                  </MenuItem>
                  <MenuItem onClick={this.handleSupportUsClick}>
                    <FontAwesomeIcon icon="heart" style={{ color: 'red' }} /> Podporiť Freemap <FontAwesomeIcon icon="heart" style={{ color: 'red' }} />
                  </MenuItem>
                </React.Fragment>
                :
                <React.Fragment>
                  <MenuItem header>
                    <FontAwesomeIcon icon="book" /> Pomoc
                  </MenuItem>
                  <MenuItem onClick={this.handleBackClick}>
                    <FontAwesomeIcon icon="chevron-left" /> Naspäť
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleLegendClick}>
                    <FontAwesomeIcon icon="map-o" /> Legenda mapy
                  </MenuItem>
                  {
                    tips.map(([key, name, icon]) => (
                      <MenuItem key={key} onSelect={this.handleTipSelect} eventKey={key}>
                        <FontAwesomeIcon icon={icon} /> {name}
                      </MenuItem>
                    ))
                  }
                  <MenuItem onClick={this.handleAboutClick}>
                    <FontAwesomeIcon icon="address-card-o" /> Kontakty
                  </MenuItem>
                </React.Fragment>
              }

            {submenu === null &&

                  <FontAwesomeIcon icon="facebook-official" />

                {' '}

                  <FontAwesomeIcon icon="twitter" />

                {' '}

                  <FontAwesomeIcon icon="github" />

            }
          </Popover>
        </Overlay>
      </React.Fragment>
    );
  }
}

export default connect(
  state => ({
    user: state.auth.user,
  }),
  dispatch => ({
    onSettingsShow() {
      dispatch(setActiveModal('settings'));
    },
    onGpxExport() {
      dispatch(setActiveModal('export-gpx'));
    },
    onShare() {
      dispatch(setActiveModal('share'));
    },
    onEmbed() {
      dispatch(setActiveModal('embed'));
    },
    onSupportUs() {
      dispatch(setActiveModal('supportUs'));
    },
    onAbout() {
      dispatch(setActiveModal('about'));
    },
    onLegend() {
      dispatch(setActiveModal('legend'));
    },
    onLocationSet(lat, lon, accuracy) {
      dispatch(setLocation(lat, lon, accuracy));
    },
    onLogin() {
      dispatch({ type: 'AUTH_CHOOSE_LOGIN_METHOD' }); // TODO to actions
      // dispatch(authLogin());
    },
    onLogout() {
      dispatch(authStartLogout());
    },
    onTip(which) {
      dispatch(tipsShow(which));
    },
  }),
)(MoreMenuButton);
<commit_msg>separated tips in the menu<commit_after>import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
import MenuItem from 'react-bootstrap/lib/MenuItem';
import Button from 'react-bootstrap/lib/Button';
import Overlay from 'react-bootstrap/lib/Overlay';
import Popover from 'react-bootstrap/lib/Popover';
import FontAwesomeIcon from 'fm3/components/FontAwesomeIcon';
import tips from 'fm3/tips/index.json';

import { setActiveModal, setLocation } from 'fm3/actions/mainActions';
import { authStartLogout } from 'fm3/actions/authActions';
import { tipsShow } from 'fm3/actions/tipsActions';

class MoreMenuButton extends React.Component {
  static propTypes = {
    onSettingsShow: PropTypes.func.isRequired,
    onGpxExport: PropTypes.func.isRequired,
    onShare: PropTypes.func.isRequired,
    onEmbed: PropTypes.func.isRequired,
    onSupportUs: PropTypes.func.isRequired,
    onAbout: PropTypes.func.isRequired,
    onLegend: PropTypes.func.isRequired,
    onLogin: PropTypes.func.isRequired,
    onLogout: PropTypes.func.isRequired,
    user: PropTypes.shape({
      name: PropTypes.string.isRequired,
    }),
    onTip: PropTypes.func.isRequired,
  };

  state = {
    show: false,
    submenu: null,
  }

  setButton = (button) => {
    this.button = button;
  };

  handleButtonClick = () => {
    this.setState({ show: true });
  }

  handleHide = () => {
    this.close();
  }

  handleItemClick = () => {
    this.close();
  }

  handleLoginClick = () => {
    this.close();
    this.props.onLogin();
  }

  handleLogoutClick = () => {
    this.close();
    this.props.onLogout();
  }

  handleShareClick = () => {
    this.close();
    this.props.onShare();
  }

  handleSettingsShowClick = () => {
    this.close();
    this.props.onSettingsShow();
  }

  handleGpxExportClick = () => {
    this.close();
    this.props.onGpxExport();
  }

  handleEmbedClick = () => {
    this.close();
    this.props.onEmbed();
  }

  handleSupportUsClick = () => {
    this.close();
    this.props.onSupportUs();
  }

  handleAboutClick = () => {
    this.close();
    this.props.onAbout();
  }

  handleLegendClick = () => {
    this.close();
    this.props.onLegend();
  }

  handleHelpClick = () => {
    this.setState({ submenu: 'help' });
  }

  handleBackClick = () => {
    this.setState({ submenu: null });
  }

  close = () => {
    this.setState({
      show: false,
      submenu: null,
    });
  }

  handleTipSelect = (tip) => {
    this.props.onTip(tip);
    this.close();
  }

  render() {
    const { user } = this.props;
    const { submenu } = this.state;

    return (
      <React.Fragment>
        <Button ref={this.setButton} onClick={this.handleButtonClick} title="Ďalšie">
          <FontAwesomeIcon icon="ellipsis-v" />
        </Button>
        <Overlay
          rootClose
          placement="bottom"
          show={this.state.show}
          onHide={this.handleHide}
          target={() => this.button}
          shouldUpdatePosition
        >
          <Popover id="popover-trigger-click-root-close" className="fm-menu">

              {submenu === null ?
                <React.Fragment>
                  {
                    user ?
                      <MenuItem onClick={this.handleLogoutClick}>
                        <FontAwesomeIcon icon="sign-out" /> Odhlásiť {user.name}
                      </MenuItem>
                      :
                      <MenuItem onClick={this.handleLoginClick}>
                        <FontAwesomeIcon icon="sign-in" /> Prihlásenie
                      </MenuItem>
                  }
                  <MenuItem onClick={this.handleSettingsShowClick}>
                    <FontAwesomeIcon icon="cog" /> Nastavenia
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleGpxExportClick}>
                    <FontAwesomeIcon icon="share" /> Exportovať do GPX
                  </MenuItem>
                  <MenuItem onClick={this.handleItemClick} href="http://wiki.freemap.sk/FileDownload" target="_blank">
                    <FontAwesomeIcon icon="mobile" /> Exporty mapy
                  </MenuItem>
                  <MenuItem onClick={this.handleShareClick}>
                    <FontAwesomeIcon icon="share-alt" /> Zdieľať mapu
                  </MenuItem>
                  <MenuItem onClick={this.handleEmbedClick}>
                    <FontAwesomeIcon icon="code" /> Vložiť do webstránky
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleItemClick} href="http://wiki.freemap.sk/NahlasenieChyby" target="_blank">
                    <FontAwesomeIcon icon="exclamation-triangle" /> Nahlásiť chybu zobrazenia v mape
                  </MenuItem>
                  <MenuItem onClick={this.handleItemClick} href="https://github.com/FreemapSlovakia/freemap-v3-react/issues" target="_blank">
                    <FontAwesomeIcon icon="exclamation-triangle" /> Nahlásiť chybu v portáli
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleHelpClick}>
                    <FontAwesomeIcon icon="book" /> Pomoc <FontAwesomeIcon icon="chevron-right" />
                  </MenuItem>
                  <MenuItem onClick={this.handleSupportUsClick}>
                    <FontAwesomeIcon icon="heart" style={{ color: 'red' }} /> Podporiť Freemap <FontAwesomeIcon icon="heart" style={{ color: 'red' }} />
                  </MenuItem>
                </React.Fragment>
                :
                <React.Fragment>
                  <MenuItem header>
                    <FontAwesomeIcon icon="book" /> Pomoc
                  </MenuItem>
                  <MenuItem onClick={this.handleBackClick}>
                    <FontAwesomeIcon icon="chevron-left" /> Naspäť
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem onClick={this.handleLegendClick}>
                    <FontAwesomeIcon icon="map-o" /> Legenda mapy
                  </MenuItem>
                  <MenuItem onClick={this.handleAboutClick}>
                    <FontAwesomeIcon icon="address-card-o" /> Kontakty
                  </MenuItem>
                  <MenuItem divider />
                  <MenuItem header>
                    <FontAwesomeIcon icon="book" /> Tipy
                  </MenuItem>
                  {
                    tips.map(([key, name, icon]) => (
                      <MenuItem key={key} onSelect={this.handleTipSelect} eventKey={key}>
                        <FontAwesomeIcon icon={icon} /> {name}
                      </MenuItem>
                    ))
                  }
                </React.Fragment>
              }

            {submenu === null &&

                  <FontAwesomeIcon icon="facebook-official" />

                {' '}

                  <FontAwesomeIcon icon="twitter" />

                {' '}

                  <FontAwesomeIcon icon="github" />

            }
          </Popover>
        </Overlay>
      </React.Fragment>
    );
  }
}

export default connect(
  state => ({
    user: state.auth.user,
  }),
  dispatch => ({
    onSettingsShow() {
      dispatch(setActiveModal('settings'));
    },
    onGpxExport() {
      dispatch(setActiveModal('export-gpx'));
    },
    onShare() {
      dispatch(setActiveModal('share'));
    },
    onEmbed() {
      dispatch(setActiveModal('embed'));
    },
    onSupportUs() {
      dispatch(setActiveModal('supportUs'));
    },
    onAbout() {
      dispatch(setActiveModal('about'));
    },
    onLegend() {
      dispatch(setActiveModal('legend'));
    },
    onLocationSet(lat, lon, accuracy) {
      dispatch(setLocation(lat, lon, accuracy));
    },
    onLogin() {
      dispatch({ type: 'AUTH_CHOOSE_LOGIN_METHOD' }); // TODO to actions
      // dispatch(authLogin());
    },
    onLogout() {
      dispatch(authStartLogout());
    },
    onTip(which) {
      dispatch(tipsShow(which));
    },
  }),
)(MoreMenuButton);
<|endoftext|>


<commit_before>var request = require('request')
  , vows = require('vows')
  , assert = require('assert')
  , captureRE = /\[([A-Z_]+)\]/g

function convertRequestHeaders(arr, sub) {
    var headers = {}, match

    for (var i = 0; i < arr.length; i++) {
        if (!(match = /([^:]+):[ \t]+(.*)/.exec(arr[i]))) continue

        if (sub)
            headers[match[1].toLowerCase()] = substitute(match[2])
        else
            headers[match[1].toLowerCase()] = match[2]
    }

    return headers
}

var api = {}

api.vars = {}

api.call = function (params) {
   return function() {
        var self = this
          , options =
            { uri: api.root + substitute(params.path)
            , method: params.method
            , headers: convertRequestHeaders(params.headers, true)
            }

        if (params.body) options.body = substitute(params.body, true)

        request(options, function (error, response, body) {
            self.callback(error, response)
        })
    }
}

function substitute(string, json) {
    for (var key in api.vars) {
        if (!api.vars.hasOwnProperty(key)) continue
        if (json)
            string = string.replace('[' + key + ']', JSON.stringify(api.vars[key]))
        else
            string = string.replace('[' + key + ']', api.vars[key])
    }
    return string
}

function headerCapture(string) {
    var captures = []
      , regex = string.replace(captureRE, function(m, name) { captures.push(name); return '(.*)' })

    if (captures.length > 0) return { regex: new RegExp(regex, 'g'), captures: captures }
}

function jsonCompare(sample, example, path) {
    var ok = true
    path = path || '/'

    function test(a, b, message) {
        assert.equal(a, b, "At key '" + path + "': expected " + a + ", got " + b)
    }

    if ('object' == typeof example && example) {
        if (example.hasOwnProperty('$WA CAPTURE')) {
            if (example['$WA CAPTURE'])
                api.vars[example['$WA CAPTURE']] = sample
        } else {
            test(typeof example, typeof sample)
            var globbing = false
            if (example['$WA GLOB']) {
                delete example['$WA GLOB']
                globbing = true
            }

            for (var key in example) {
                if (!example.hasOwnProperty(key)) continue
                assert.ok(sample.hasOwnProperty(key), "At " + path + ", key '" + key + "' not found")
                jsonCompare(sample[key], example[key], path + '/' + key)
            }
            if (!globbing) {
                for (var key in sample) {
                    if (!sample.hasOwnProperty(key)) continue
                    assert.ok(example.hasOwnProperty(key), "At " + path + ", unexpected key \"" + key + "\": " + JSON.stringify(sample[key]) )
                }
            }
        }
    } else {
        test(typeof example, typeof sample)
        test(example, sample)
    }
}

function unicode(str) {
    var output = '', code
    for (var i = 0; i < str.length; i++) {
        code = str.charCodeAt(i).toString(16)
        while (code.length < 4) code = '0' + code
        output += '\\u' + code
    }
    return output
}

function escapeString(string) {
    return string.replace(/(\*|\.\.\.|\[)/, unicode)
}

api.matchers = function (batch, params) {
    batch[params.label] = function(error, result) {
        assert.equal(result.statusCode, params.code, "Got status code: " + result.statusCode)
    }

    var headers = convertRequestHeaders(params.headers)
    for (var key in headers) {
        batch[key + ": " + headers[key]] = function(error, result) {
            var pattern, header = substitute(headers[key])
            if (pattern = headerCapture(header)) {
                assert.ok(result.headers[key], "Header should exist, but doesn't")
                var match = pattern.regex.exec(result.headers[key])
                assert.ok(match, "Header did not match pattern")
                for (var i = 0; i < pattern.captures.length; i++) {
                    assert.ok(match[i], pattern.captures.length, "Header did not match all patterns")
                    api.vars[pattern.captures[i]] = match[i + 1]
                }
            } else {
                assert.equal(result.headers[key], headers[key])
            }
        }
    }

    if (params.body) {
        batch[params.body.replace(/\s+/g, " ")] = function(error, result) {
            var pattern = substitute(params.body, true), body
            pattern = pattern.replace(/".*?[^\\"]"/g, escapeString)
            pattern = pattern.replace(/\*/g, '{ "$WA CAPTURE": false }')
            pattern = pattern.replace(/\.\.\./g, '"$WA GLOB": true')
            pattern = pattern.replace(captureRE, '{ "$WA CAPTURE": "$1" }')
            pattern = JSON.parse(pattern)

            assert.notEqual(result.body, '', "No body returned")
            assert.doesNotThrow(function(){ body = JSON.parse(result.body) }, SyntaxError, "Response was not JSON: " + result.body)
            jsonCompare(JSON.parse(result.body), pattern)
        }
    } else {
        batch["Empty response"] = function(error, result) {
            assert.equal(result.body, '', "Body should be empty")
        }
    }
}

api.makeBatch = function (test) {
    var batch = {}, topic = {}

    batch[test.req.label] = topic = { topic: api.call(test.req) }
    if (test.resp)
        api.matchers(topic, test.resp);
    else
        topic['done'] = function(err, result) { assert.ok(result) }

    return batch;
}

api.compile = function (results) {
    var suite = vows.describe(results.title)

    for (var i = 0; i < results.tests.length; i++) {
        suite.addBatch(api.makeBatch(results.tests[i]))
    }

    return suite
}

module.exports = api

<commit_msg>If the expected response isn't JSON, match literally<commit_after>var request = require('request')
  , vows = require('vows')
  , assert = require('assert')
  , captureRE = /\[([A-Z_]+)\]/g

function convertRequestHeaders(arr, sub) {
    var headers = {}, match

    for (var i = 0; i < arr.length; i++) {
        if (!(match = /([^:]+):[ \t]+(.*)/.exec(arr[i]))) continue

        if (sub)
            headers[match[1].toLowerCase()] = substitute(match[2])
        else
            headers[match[1].toLowerCase()] = match[2]
    }

    return headers
}

var api = {}

api.vars = {}

api.call = function (params) {
   return function() {
        var self = this
          , options =
            { uri: api.root + substitute(params.path)
            , method: params.method
            , headers: convertRequestHeaders(params.headers, true)
            }

        if (params.body) options.body = substitute(params.body, true)

        request(options, function (error, response, body) {
            self.callback(error, response)
        })
    }
}

function substitute(string, json) {
    for (var key in api.vars) {
        if (!api.vars.hasOwnProperty(key)) continue
        if (json)
            string = string.replace('[' + key + ']', JSON.stringify(api.vars[key]))
        else
            string = string.replace('[' + key + ']', api.vars[key])
    }
    return string
}

function headerCapture(string) {
    var captures = []
      , regex = string.replace(captureRE, function(m, name) { captures.push(name); return '(.*)' })

    if (captures.length > 0) return { regex: new RegExp(regex, 'g'), captures: captures }
}

function jsonCompare(sample, example, path) {
    var ok = true
    path = path || '/'

    function test(a, b, message) {
        assert.equal(a, b, "At key '" + path + "': expected " + a + ", got " + b)
    }

    if ('object' == typeof example && example) {
        if (example.hasOwnProperty('$WA CAPTURE')) {
            if (example['$WA CAPTURE'])
                api.vars[example['$WA CAPTURE']] = sample
        } else {
            test(typeof example, typeof sample)
            var globbing = false
            if (example['$WA GLOB']) {
                delete example['$WA GLOB']
                globbing = true
            }

            for (var key in example) {
                if (!example.hasOwnProperty(key)) continue
                assert.ok(sample.hasOwnProperty(key), "At " + path + ", key '" + key + "' not found")
                jsonCompare(sample[key], example[key], path + '/' + key)
            }
            if (!globbing) {
                for (var key in sample) {
                    if (!sample.hasOwnProperty(key)) continue
                    assert.ok(example.hasOwnProperty(key), "At " + path + ", unexpected key \"" + key + "\": " + JSON.stringify(sample[key]) )
                }
            }
        }
    } else {
        test(typeof example, typeof sample)
        test(example, sample)
    }
}

function unicode(str) {
    var output = '', code
    for (var i = 0; i < str.length; i++) {
        code = str.charCodeAt(i).toString(16)
        while (code.length < 4) code = '0' + code
        output += '\\u' + code
    }
    return output
}

function escapeString(string) {
    return string.replace(/(\*|\.\.\.|\[)/, unicode)
}

api.matchers = function (batch, params) {
    batch[params.label] = function(error, result) {
        assert.equal(result.statusCode, params.code, "Got status code: " + result.statusCode)
    }

    var headers = convertRequestHeaders(params.headers)
    for (var key in headers) {
        batch[key + ": " + headers[key]] = function(error, result) {
            var pattern, header = substitute(headers[key])
            if (pattern = headerCapture(header)) {
                assert.ok(result.headers[key], "Header should exist, but doesn't")
                var match = pattern.regex.exec(result.headers[key])
                assert.ok(match, "Header did not match pattern")
                for (var i = 0; i < pattern.captures.length; i++) {
                    assert.ok(match[i], pattern.captures.length, "Header did not match all patterns")
                    api.vars[pattern.captures[i]] = match[i + 1]
                }
            } else {
                assert.equal(result.headers[key], headers[key])
            }
        }
    }

    if (params.body) {
        batch[params.body.replace(/\s+/g, " ")] = function(error, result) {
            var pattern = substitute(params.body, true), body
            pattern = pattern.replace(/".*?[^\\"]"/g, escapeString)
            pattern = pattern.replace(/\*/g, '{ "$WA CAPTURE": false }')
            pattern = pattern.replace(/\.\.\./g, '"$WA GLOB": true')
            pattern = pattern.replace(captureRE, '{ "$WA CAPTURE": "$1" }')
            try {
                pattern = JSON.parse(pattern)
            } catch(e) {
                assert.equal(result.body, params.body)
            }
            assert.notEqual(result.body, '', "No body returned")
            assert.doesNotThrow(function(){ body = JSON.parse(result.body) }, SyntaxError, "Response was not JSON: " + result.body)
            jsonCompare(JSON.parse(result.body), pattern)
        }
    } else {
        batch["Empty response body"] = function(error, result) {
            assert.equal(result.body, '', "Body should be empty")
        }
    }
}

api.makeBatch = function (test) {
    var batch = {}, topic = {}

    batch[test.req.label] = topic = { topic: api.call(test.req) }
    if (test.resp)
        api.matchers(topic, test.resp);
    else
        topic['done'] = function(err, result) { assert.ok(result) }

    return batch;
}

api.compile = function (results) {
    var suite = vows.describe(results.title)

    for (var i = 0; i < results.tests.length; i++) {
        suite.addBatch(api.makeBatch(results.tests[i]))
    }

    return suite
}

module.exports = api

<|endoftext|>


<commit_before>import React from 'react';
import './EditorForm.scss';
import update from 'immutability-helper';
// import find from 'lodash/find';

export class EditorForm extends React.Component {
  constructor (props) {
    super(props);
    this.generateAttributeElements = this.generateAttributeElements.bind(this);
    this.getActiveValue = this.getActiveValue.bind(this);
    this.getCheckedState = this.getCheckedState.bind(this);
    this.closeEditorForm = this.closeEditorForm.bind(this);
    this.state = {
      newAttributes: this.initializeAttributes(this.props.attributeTypes),
      recordName: {
        value: this.props.recordConfig.recordName || '',
        checked: true
      },
      recordType: {
        value: this.props.attributes.RecordType || '',
        checked: true
      }
    };
  }

  initializeAttributes (attributeTypes) {
    const { attributes } = this.props;
    let initialState = {};
    for (const key in attributeTypes) {
      if (attributeTypes.hasOwnProperty(key)) {
        initialState = Object.assign({}, initialState, {
          [key]: {
            name: attributes[key] ? attributes[key] : null,
            checked: true
          }
        });
      }
    };
    return initialState;
  }

  // findRecordTypeFromList (recordType) {
  //   const correctType = find(this.props.recordTypes, (type) => (
  //     type.name === recordType
  //   ));
  //   return correctType;
  // }

  onChange (e, key, field) {
    const newValue = e;
    this.setState(update(this.state, {
      newAttributes: {
        [key]: {
          [field]: {
            $set: newValue
          }
        }
      }
    }));
  }

  onBaseAttributeChange (e, key, field) {
    const newValue = e;
    this.setState(update(this.state, {
      [key]: {
        [field]: {
          $set: newValue
        }
      }
    }));
  }

  getActiveValue (key) {
    if (this.state.newAttributes[key].name) {
      return this.state.newAttributes[key].name;
    }
  }

  getCheckedState (key) {
    if (this.props.editorConfig.action === 'edit' && !this.state.newAttributes[key].name) {
      return false;
    } else {
      return this.state.newAttributes[key].checked;
    }
  }

  generateAttributeElements (attributeTypes) {
    const attributeElements = [];
    for (const key in attributeTypes) {
      if (attributeTypes.hasOwnProperty(key)) {
        if (attributeTypes[key].values.length) {
          const options = attributeTypes[key].values.map((option, index) => (
            <option key={index} value={option.value}>{option.value}</option>
          ));
          attributeElements.push(

              <input
                type='checkbox'
                checked={this.getCheckedState(key)}
                value={this.state.newAttributes[key].checked}
                onChange={(e) => this.onChange(!this.state.newAttributes[key].checked, key, 'checked')}/>

                {attributeTypes[key].name}
                { attributeTypes[key].required &&

                }

              <select
                value={this.getActiveValue(key)}
                className='form-control edit-record__select'
                onChange={(e) => this.onChange(e.target.value, key, 'name')}
                disabled={!this.state.newAttributes[key].checked}>
                <option value={null}>[ Tyhjä ]</option>
                { options }
              </select>

          );
        } else if (attributeTypes[key].values.length === 0) {
          attributeElements.push(

              <input
                type='checkbox'
                checked={this.getCheckedState(key)}
                value={this.state.newAttributes[key].checked}
                onChange={(e) => this.onChange(!this.state.newAttributes[key].checked, key, 'checked')}
              />

                {attributeTypes[key].name}
                { attributeTypes[key].required &&

                }

              <input
                className='form-control edit-record__input'
                value={this.getActiveValue(key)}
                placeholder={attributeTypes[key].name}
                onChange={(e) => this.onChange(e.target.value, key, 'name')}
                disabled={!this.state.newAttributes[key].checked}
              />

          );
        }
      }
    }
    return attributeElements;
  }

  generateDropdown (recordTypes) {
    const options = [];
    for (const key in recordTypes) {
      if (recordTypes.hasOwnProperty(key)) {
        options.push(
          <option key={key} value={recordTypes[key].name}>
            {recordTypes[key].name}
          </option>
        );
      }
    }
    return (
      <select
        value={this.state.recordType.value}
        className='form-control col-xs-6'
        onChange={(e) => this.onBaseAttributeChange(e.target.value, 'recordType', 'value')}>
        <option value={null}>[ Tyhjä ]</option>
        {options}
      </select>
    );
  }

  addRecord (e, targetId) {
    e.preventDefault();
    const { recordName, recordType, newAttributes } = this.state;
    this.props.recordConfig.createRecord(targetId, recordName.value, recordType.value, newAttributes);
    this.props.displayMessage({
      text: 'Asiakirjan lisäys onnistui!',
      success: true
    });
  }

  editRecord (e, targetId) {
    e.preventDefault();
    const { recordName, recordType, newAttributes } = this.state;
    this.props.recordConfig.editRecordWithForm(targetId, recordName.value, recordType.value, newAttributes);
    this.props.displayMessage({
      text: 'Asiakirjan muokkaus onnistui!',
      success: true
    });
  }

  resolveOnSubmit (e, targetId) {
    const { action, type } = this.props.editorConfig;

    switch (type) {
      case 'tos':
        if (action === 'edit') {
          // this.editMetaData(e, targetId);
        }
        break;
      case 'phase':
        if (action === 'add') {
          // this.addPhase(e, targetId);
        }
        if (action === 'edit') {
          // this.editPhase(e, targetId);
        }
        break;
      case 'action':
        if (action === 'add') {
          // this.addAction(e, targetId);
        }
        if (action === 'edit') {
          // this.editAction(e, targetId);
        }
        break;
      case 'record':
        if (action === 'add') {
          this.addRecord(e, targetId);
        }
        if (action === 'edit') {
          this.editRecord(e, targetId);
        }
    }
  }

  closeEditorForm (e) {
    e.preventDefault();
    this.props.closeEditorForm();
  }

  render () {
    const { attributeTypes, targetId } = this.props;
    const attributeElements = this.generateAttributeElements(attributeTypes);
    const typeDropdown = this.generateDropdown(this.props.recordConfig.recordTypes);
    return (

{ this.props.recordConfig.editRecordWithForm ? 'Muokkaa asiakirjaa' : 'Uusi asiakirja' }

        <form onSubmit={(e) => this.resolveOnSubmit(e, targetId)}
              className='edit-record'>

            Asiakirjatyypin tarkenne

            <input
              className='col-xs-6 form-control edit-record__input'
              placeholder='Tarkenne'
              value={this.state.recordName.value}
              onChange={(e) => this.onBaseAttributeChange(e.target.value, 'recordName', 'value')}/>

            Tyyppi

            { typeDropdown }

          { attributeElements }

            <button className='btn btn-primary pull-right edit-record__submit' type='submit'>Valmis</button>
            <button
              className='btn btn-danger pull-right edit-record__cancel'
              onClick={(e) => this.closeEditorForm(e)}>
              Peruuta
            </button>

        </form>

    );
  }
}

EditorForm.propTypes = {
  attributeTypes: React.PropTypes.object.isRequired,
  attributes: React.PropTypes.object.isRequired,
  closeEditorForm: React.PropTypes.func.isRequired,
  displayMessage: React.PropTypes.func.isRequired,
  // editMetaData: React.PropTypes.object,
  editorConfig: React.PropTypes.shape({
    type: React.PropTypes.string.isRequired,
    action: React.PropTypes.string.isRequired
  }),
  recordConfig: React.PropTypes.shape({
    editRecordWithForm: React.PropTypes.func,
    recordTypes: React.PropTypes.object,
    createRecord: React.PropTypes.func,
    recordId: React.PropTypes.string,
    recordName: React.PropTypes.string
  }),
  targetId: React.PropTypes.string
};

export default EditorForm;
<commit_msg>Create a function to resolve form heading label<commit_after>import React from 'react';
import './EditorForm.scss';
import update from 'immutability-helper';
// import find from 'lodash/find';

export class EditorForm extends React.Component {
  constructor (props) {
    super(props);
    this.generateAttributeElements = this.generateAttributeElements.bind(this);
    this.getActiveValue = this.getActiveValue.bind(this);
    this.getCheckedState = this.getCheckedState.bind(this);
    this.closeEditorForm = this.closeEditorForm.bind(this);
    this.state = {
      newAttributes: this.initializeAttributes(this.props.attributeTypes),
      recordName: {
        value: this.props.recordConfig.recordName || '',
        checked: true
      },
      recordType: {
        value: this.props.attributes.RecordType || '',
        checked: true
      }
    };
  }

  initializeAttributes (attributeTypes) {
    const { attributes } = this.props;
    let initialState = {};
    for (const key in attributeTypes) {
      if (attributeTypes.hasOwnProperty(key)) {
        initialState = Object.assign({}, initialState, {
          [key]: {
            name: attributes[key] ? attributes[key] : null,
            checked: true
          }
        });
      }
    };
    return initialState;
  }

  // findRecordTypeFromList (recordType) {
  //   const correctType = find(this.props.recordTypes, (type) => (
  //     type.name === recordType
  //   ));
  //   return correctType;
  // }

  onChange (e, key, field) {
    const newValue = e;
    this.setState(update(this.state, {
      newAttributes: {
        [key]: {
          [field]: {
            $set: newValue
          }
        }
      }
    }));
  }

  onBaseAttributeChange (e, key, field) {
    const newValue = e;
    this.setState(update(this.state, {
      [key]: {
        [field]: {
          $set: newValue
        }
      }
    }));
  }

  getActiveValue (key) {
    if (this.state.newAttributes[key].name) {
      return this.state.newAttributes[key].name;
    }
  }

  getCheckedState (key) {
    if (this.props.editorConfig.action === 'edit' && !this.state.newAttributes[key].name) {
      return false;
    } else {
      return this.state.newAttributes[key].checked;
    }
  }

  generateAttributeElements (attributeTypes) {
    const attributeElements = [];
    for (const key in attributeTypes) {
      if (attributeTypes.hasOwnProperty(key)) {
        if (attributeTypes[key].values.length) {
          const options = attributeTypes[key].values.map((option, index) => (
            <option key={index} value={option.value}>{option.value}</option>
          ));
          attributeElements.push(

              <input
                type='checkbox'
                checked={this.getCheckedState(key)}
                value={this.state.newAttributes[key].checked}
                onChange={(e) => this.onChange(!this.state.newAttributes[key].checked, key, 'checked')}/>

                {attributeTypes[key].name}
                { attributeTypes[key].required &&

                }

              <select
                value={this.getActiveValue(key)}
                className='form-control edit-record__select'
                onChange={(e) => this.onChange(e.target.value, key, 'name')}
                disabled={!this.state.newAttributes[key].checked}>
                <option value={null}>[ Tyhjä ]</option>
                { options }
              </select>

          );
        } else if (attributeTypes[key].values.length === 0) {
          attributeElements.push(

              <input
                type='checkbox'
                checked={this.getCheckedState(key)}
                value={this.state.newAttributes[key].checked}
                onChange={(e) => this.onChange(!this.state.newAttributes[key].checked, key, 'checked')}
              />

                {attributeTypes[key].name}
                { attributeTypes[key].required &&

                }

              <input
                className='form-control edit-record__input'
                value={this.getActiveValue(key)}
                placeholder={attributeTypes[key].name}
                onChange={(e) => this.onChange(e.target.value, key, 'name')}
                disabled={!this.state.newAttributes[key].checked}
              />

          );
        }
      }
    }
    return attributeElements;
  }

  generateDropdown (recordTypes) {
    const options = [];
    for (const key in recordTypes) {
      if (recordTypes.hasOwnProperty(key)) {
        options.push(
          <option key={key} value={recordTypes[key].name}>
            {recordTypes[key].name}
          </option>
        );
      }
    }
    return (
      <select
        value={this.state.recordType.value}
        className='form-control col-xs-6'
        onChange={(e) => this.onBaseAttributeChange(e.target.value, 'recordType', 'value')}>
        <option value={null}>[ Tyhjä ]</option>
        {options}
      </select>
    );
  }

  addRecord (e, targetId) {
    e.preventDefault();
    const { recordName, recordType, newAttributes } = this.state;
    this.props.recordConfig.createRecord(targetId, recordName.value, recordType.value, newAttributes);
    this.props.displayMessage({
      text: 'Asiakirjan lisäys onnistui!',
      success: true
    });
  }

  editRecord (e, targetId) {
    e.preventDefault();
    const { recordName, recordType, newAttributes } = this.state;
    this.props.recordConfig.editRecordWithForm(targetId, recordName.value, recordType.value, newAttributes);
    this.props.displayMessage({
      text: 'Asiakirjan muokkaus onnistui!',
      success: true
    });
  }

  resolveLabel () {
    const { type, action } = this.props.editorConfig;

    switch (type) {
      case 'tos':
        return 'Muokkaa metatietoja';
      case 'phase':
        return action === 'edit' ? 'Muokkaa käsittelyvaihetta' : 'Uusi käsittelyvaihe';
      case 'action':
        return action === 'edit' ? 'Muokkaa toimenpidettä' : 'Uusi toimenpide';
      case 'record':
        return action === 'edit' ? 'Muokkaa asiakirjaa' : 'Uusi asiakirja';
    }
  }

  resolveOnSubmit (e, targetId) {
    const { action, type } = this.props.editorConfig;

    switch (type) {
      case 'tos':
        if (action === 'edit') {
          // this.editMetaData(e, targetId);
        }
        break;
      case 'phase':
        if (action === 'add') {
          // this.addPhase(e, targetId);
        }
        if (action === 'edit') {
          // this.editPhase(e, targetId);
        }
        break;
      case 'action':
        if (action === 'add') {
          // this.addAction(e, targetId);
        }
        if (action === 'edit') {
          // this.editAction(e, targetId);
        }
        break;
      case 'record':
        if (action === 'add') {
          this.addRecord(e, targetId);
        }
        if (action === 'edit') {
          this.editRecord(e, targetId);
        }
    }
  }

  closeEditorForm (e) {
    e.preventDefault();
    this.props.closeEditorForm();
  }

  render () {
    const { attributeTypes, targetId } = this.props;
    const attributeElements = this.generateAttributeElements(attributeTypes);
    const typeDropdown = this.generateDropdown(this.props.recordConfig.recordTypes);
    return (

{this.resolveLabel()}

        <form onSubmit={(e) => this.resolveOnSubmit(e, targetId)}
              className='edit-record'>

            Asiakirjatyypin tarkenne

            <input
              className='col-xs-6 form-control edit-record__input'
              placeholder='Tarkenne'
              value={this.state.recordName.value}
              onChange={(e) => this.onBaseAttributeChange(e.target.value, 'recordName', 'value')}/>

            Tyyppi

            { typeDropdown }

          { attributeElements }

            <button className='btn btn-primary pull-right edit-record__submit' type='submit'>Valmis</button>
            <button
              className='btn btn-danger pull-right edit-record__cancel'
              onClick={(e) => this.closeEditorForm(e)}>
              Peruuta
            </button>

        </form>

    );
  }
}

EditorForm.propTypes = {
  attributeTypes: React.PropTypes.object.isRequired,
  attributes: React.PropTypes.object.isRequired,
  closeEditorForm: React.PropTypes.func.isRequired,
  displayMessage: React.PropTypes.func.isRequired,
  // editMetaData: React.PropTypes.object,
  editorConfig: React.PropTypes.shape({
    type: React.PropTypes.string.isRequired,
    action: React.PropTypes.string.isRequired
  }),
  recordConfig: React.PropTypes.shape({
    editRecordWithForm: React.PropTypes.func,
    recordTypes: React.PropTypes.object,
    createRecord: React.PropTypes.func,
    recordId: React.PropTypes.string,
    recordName: React.PropTypes.string
  }),
  targetId: React.PropTypes.string
};

export default EditorForm;
<|endoftext|>


<commit_before>import _findIndex from "lodash/findIndex";
import _findLastIndex from "lodash/findLastIndex";
import _max from "lodash/max";
import { line, curveBasis, arc } from "d3-shape";
import { easeLinear } from "d3-ease";
import { demeCountMultiplier, demeCountMinimum } from "../../util/globals";
import { updateTipRadii } from "../../actions/tree";

/* util */

export const pathStringGenerator = line()
  .x((d) => { return d.x; })
  .y((d) => { return d.y; })
  .curve(curveBasis);

const extractLineSegmentForAnimationEffect = (
  numDateMin,
  numDateMax,
  originCoords,
  destinationCoords,
  originNumDate,
  destinationNumDate,
  visible,
  bezierCurve,
  bezierDates
) => {

  if (visible === "hidden") {
    return [];
  }

  // want to slice out all points that lie between numDateMin and numDateMax
  // and append interpolated start and end points
  // initial data
  // bezierDates = [ 2015.1    2015.2    2015.3    2015.4    2015.5 ]
  // bezierCurve = [ x0,y0     x1,y1     x2,y2     x3,y3     x4,y4  ]
  //
  // scenario A: numDateMin = 2015.25, numDateMax = 2015.45
  // bezierDates = [ 2015.25  2015.3    2015.4  2015.45 ]
  // bezierCurve = [ x12,y12  x2,y2     x3,y3   x34,y34 ]
  // startIndex is 2 in scenario A
  // endIndex is 3 in scenario A
  //
  // scenario B: numDateMin = 2014.5, numDateMax = 2015.45
  // bezierDates = [ 2015.1    2015.2    2015.3    2015.4  2015.45 ]
  // bezierCurve = [ x0,y0     x1,y1     x2,y2     x3,y3   x34,y34 ]
  // startIndex is 0 in scenario B
  // endIndex is 3 in scenario B
  //
  // scenario C: numDateMin = 2015.25, numDateMax = 2015.7
  // bezierDates = [ 2015.25  2015.3    2015.4    2015.5 ]
  // bezierCurve = [ x12,y12  x2,y2     x3,y3     x4,y4  ]
  // startIndex is 2 in scenario C
  // endIndex is 4 in scenario C
  //
  // scenario D: numDateMin = 2015.6, numDateMax = 2015.9
  // bezierDates = [ ]
  // bezierCurve = [ ]
  // startIndex is -1 in scenario D
  // endIndex is 4 in scenario D

  // find start walking forwards from left of array
  const startIndex = _findIndex(bezierDates, (d) => { return d > numDateMin; });

  // find end walking backwards from right of array
  const endIndex = _findLastIndex(bezierDates, (d) => { return d < numDateMax; });

  // startIndex and endIndex is -1 if not found
  // this indicates a slice of time that lies outside the bounds of BCurve
  // return empty array
  if (startIndex === -1 || endIndex === -1) {
    return [];
  }

  // get curve
  // slice takes index at begin
  // slice extracts up to but not including end
  const curve = bezierCurve.slice(startIndex, endIndex + 1);

  // if possible construct and prepend interpolated start
  let newStart;
  if (startIndex > 0) {
    // determine weighting of positions at startIndex and startIndex-1
    const dateDiff = bezierDates[startIndex] - bezierDates[startIndex - 1];
    const weightRight = (numDateMin - bezierDates[startIndex - 1]) / dateDiff;
    const weightLeft = (bezierDates[startIndex] - numDateMin) / dateDiff;
    // construct interpolated new start
    newStart = {
      x: (weightLeft * bezierCurve[startIndex - 1].x) + (weightRight * bezierCurve[startIndex].x),
      y: (weightLeft * bezierCurve[startIndex - 1].y) + (weightRight * bezierCurve[startIndex].y)
    };
    // will break indexing, so wait to prepend
  }

  // if possible construct and prepend interpolated start
  let newEnd;
  if (endIndex < bezierCurve.length - 1) {
    // determine weighting of positions at startIndex and startIndex-1
    const dateDiff = bezierDates[endIndex + 1] - bezierDates[endIndex];
    const weightRight = (numDateMax - bezierDates[endIndex]) / dateDiff;
    const weightLeft = (bezierDates[endIndex + 1] - numDateMax) / dateDiff;
    // construct interpolated new end
    newEnd = {
      x: (weightLeft * bezierCurve[endIndex].x) + (weightRight * bezierCurve[endIndex + 1].x),
      y: (weightLeft * bezierCurve[endIndex].y) + (weightRight * bezierCurve[endIndex + 1].y)
    };
    // will break indexing, so wait to append
  }

  // prepend / append interpolated points if they exist
  if (newStart) {
    curve.unshift(newStart);
  }
  if (newEnd) {
    curve.push(newEnd);
  }
  return curve;
};

const createArcsFromDemes = (demeData) => {
  const individualArcs = [];
  demeData.forEach((demeInfo) => {
    demeInfo.arcs.forEach((slice) => {
      individualArcs.push(slice);
    });
  });
  return individualArcs;
};

export const drawDemesAndTransmissions = (
  demeData,
  transmissionData,
  g,
  map,
  nodes,
  numDateMin,
  numDateMax,
  pieChart, /* bool */
  geoResolution,
  dispatch,
  showTransmissionLines
) => {

  // add transmission lines
  const transmissions = g.selectAll("transmissions")
    .data(transmissionData)
    .enter()
    .append("path") /* instead of appending a geodesic path from the leaflet plugin data, we now draw a line directly between two points */
    .attr("d", (d) => {
      return pathStringGenerator(
          extractLineSegmentForAnimationEffect(
          numDateMin,
          numDateMax,
          d.originCoords,
          d.destinationCoords,
          d.originNumDate,
          d.destinationNumDate,
          d.visible,
          d.bezierCurve,
          d.bezierDates
          )
      );
    })
    .attr("fill", "none")
    .attr("stroke-opacity", 0.6)
    .attr("stroke-linecap", "round")
    .attr("stroke", (d) => { return d.color; })
    .attr("stroke-width", showTransmissionLines ? 1 : 0);

  const visibleTips = nodes[0].tipCount;
  const demeMultiplier =
    demeCountMultiplier /
    Math.sqrt(_max([Math.sqrt(visibleTips * nodes.length), demeCountMinimum]));
  let demes;
  // determine whether to draw pieChart or not (sensible for categorical data)
  if (pieChart) {
    /* each datapoint in `demeData` contains `arcs` which comprise n individual "slices"
     * we need to create an array of all of these slices for d3 to render
     */
    const individualArcs = createArcsFromDemes(demeData);

    /* add `outerRadius` to all slices */
    // TODO - move this to initial arc creation in setupDemeData as it's only ever done once
    individualArcs.forEach((a) => {
      a.outerRadius = Math.sqrt(demeData[a.demeDataIdx].count)*demeMultiplier;
    });

    demes = g.selectAll('demes') // add individual arcs ("slices") to this selection
      .data(individualArcs)
      .enter().append("path")
      .attr("d", (d) => arc()(d))
      /* following calls are (almost) the same for pie charts & circles */
      .style("stroke", "none")
      .style("fill-opacity", 0.65)
      .style("fill", (d) => { return d.color; })
      .style("stroke-opacity", 0.85)
      .style("stroke", (d) => { return d.color; })
      .style("pointer-events", "all")
      .attr("transform", (d) =>
        "translate(" + demeData[d.demeDataIdx].coords.x + "," + demeData[d.demeDataIdx].coords.y + ")"
      )
      .on("mouseover", (d) => { dispatch(updateTipRadii({geoFilter: [geoResolution, demeData[d.demeDataIdx].name]})); })
      .on("mouseout", () => { dispatch(updateTipRadii()); });
  } else {
    demes = g.selectAll("demes") // add deme circles to this selection
      .data(demeData)
      .enter().append("circle")
      .attr("r", (d) => { return demeMultiplier * Math.sqrt(d.count); })
      /* following calls are (almost) the same for pie charts & circles */
      .style("stroke", "none")
      .style("fill-opacity", 0.65)
      .style("fill", (d) => { return d.color; })
      .style("stroke-opacity", 0.85)
      .style("stroke", (d) => { return d.color; })
      .style("pointer-events", "all")
      .attr("transform", (d) => "translate(" + d.coords.x + "," + d.coords.y + ")")
      .on("mouseover", (d) => { dispatch(updateTipRadii({geoFilter: [geoResolution, d.name]})); })
      .on("mouseout", () => { dispatch(updateTipRadii()); });
  }

  return {
    demes,
    transmissions
  };

};

export const updateOnMoveEnd = (demeData, transmissionData, d3elems, numDateMin, numDateMax, pieChart) => {
  /* map has moved or rescaled, make demes and transmissions line up */
  if (!d3elems) { return; }

  /* move the pie charts differently to the color-blended circles */
  if (pieChart) {
    const individualArcs = createArcsFromDemes(demeData);
    d3elems.demes
      .data(individualArcs)
      .attr("transform", (d) =>
        /* copied from above. TODO. */
        "translate(" + demeData[d.demeDataIdx].coords.x + "," + demeData[d.demeDataIdx].coords.y + ")"
      );
  } else {
    d3elems.demes
      .data(demeData)
      .attr("transform", (d) =>
        /* copied from above. TODO. */
        "translate(" + d.coords.x + "," + d.coords.y + ")"
      );
  }

  d3elems.transmissions
    .data(transmissionData)
    .attr("d", (d) => {
      return pathStringGenerator(
        extractLineSegmentForAnimationEffect(
          numDateMin,
          numDateMax,
          d.originCoords,
          d.destinationCoords,
          d.originNumDate,
          d.destinationNumDate,
          d.visible,
          d.bezierCurve,
          d.bezierDates
        )
      );
    }); // other attrs remain the same as they were
};

export const updateVisibility = (
  demeData,
  transmissionData,
  d3elems,
  map,
  nodes,
  numDateMin,
  numDateMax,
  pieChart
) => {

  if (!d3elems) {
    console.error("d3elems is not defined!");
    return;
  }
  const visibleTips = nodes[0].tipCount;
  const demeMultiplier =
    demeCountMultiplier /
    Math.sqrt(_max([Math.sqrt(visibleTips * nodes.length), demeCountMinimum]));

  if (pieChart) {
    const individualArcs = createArcsFromDemes(demeData);
    /* add `outerRadius` to all slices */
    // TODO - move this to initial arc creation in setupDemeData as it's only ever done once
    individualArcs.forEach((a) => {
      a.outerRadius = Math.sqrt(demeData[a.demeDataIdx].count)*demeMultiplier;
    });
    d3elems.demes
      .data(individualArcs)
      .attr("d", (d) => arc()(d))
      .style("fill", (d) => { return d.color; })
      .style("stroke", (d) => { return d.color; });
  } else {
    /* for colour blended circles we just have to update the colours & size (radius) */
    d3elems.demes
      .data(demeData)
      .transition()
      .duration(200)
      .ease(easeLinear)
      .style("stroke", (d) => { return d.count > 0 ? d.color : "white"; })
      .style("fill", (d) => { return d.count > 0 ? d.color : "white"; })
      .attr("r", (d) => { return demeMultiplier * Math.sqrt(d.count); });
  }

  /* update the path and stroke colour of transmission lines */
  d3elems.transmissions
    .data(transmissionData)
    .attr("d", (d) => {
      return pathStringGenerator(
        extractLineSegmentForAnimationEffect(
          numDateMin,
          numDateMax,
          d.originCoords,
          d.destinationCoords,
          d.originNumDate,
          d.destinationNumDate,
          d.visible,
          d.bezierCurve,
          d.bezierDates
        )
      );
    }) /* with the interpolation in the function above pathStringGenerator */
    .attr("stroke", (d) => { return d.color; });
};

/* template for an update helper */
export const updateFoo = (d3elems, latLongs) => {
  d3elems.demes
    .data(latLongs.demes);

  d3elems.transmissions
    .data(latLongs.transmissions);
};

/*
  http://gis.stackexchange.com/questions/49114/d3-geo-path-to-draw-a-path-from-gis-coordinates
  https://bl.ocks.org/mbostock/3916621  // Compute point-interpolators at each distance.
  http://bl.ocks.org/mbostock/5851933 // draw line on map
  https://gist.github.com/mikeatlas/0b69b354a8d713989147 // polyline split if we don't use leaflet
  http://bl.ocks.org/mbostock/5928813
  http://bl.ocks.org/duopixel/4063326 // animate path in
  https://bl.ocks.org/mbostock/1705868 // point along path interpolation
  https://bl.ocks.org/mbostock/1313857 // point along path interpolation
  https://github.com/d3/d3-shape/blob/master/README.md#curves
  https://github.com/d3/d3-shape/blob/master/README.md#lines
*/
<commit_msg>use spaces<commit_after>import _findIndex from "lodash/findIndex";
import _findLastIndex from "lodash/findLastIndex";
import _max from "lodash/max";
import { line, curveBasis, arc } from "d3-shape";
import { easeLinear } from "d3-ease";
import { demeCountMultiplier, demeCountMinimum } from "../../util/globals";
import { updateTipRadii } from "../../actions/tree";

/* util */

export const pathStringGenerator = line()
  .x((d) => { return d.x; })
  .y((d) => { return d.y; })
  .curve(curveBasis);

const extractLineSegmentForAnimationEffect = (
  numDateMin,
  numDateMax,
  originCoords,
  destinationCoords,
  originNumDate,
  destinationNumDate,
  visible,
  bezierCurve,
  bezierDates
) => {

  if (visible === "hidden") {
    return [];
  }

  // want to slice out all points that lie between numDateMin and numDateMax
  // and append interpolated start and end points
  // initial data
  // bezierDates = [ 2015.1    2015.2    2015.3    2015.4    2015.5 ]
  // bezierCurve = [ x0,y0     x1,y1     x2,y2     x3,y3     x4,y4  ]
  //
  // scenario A: numDateMin = 2015.25, numDateMax = 2015.45
  // bezierDates = [ 2015.25  2015.3    2015.4  2015.45 ]
  // bezierCurve = [ x12,y12  x2,y2     x3,y3   x34,y34 ]
  // startIndex is 2 in scenario A
  // endIndex is 3 in scenario A
  //
  // scenario B: numDateMin = 2014.5, numDateMax = 2015.45
  // bezierDates = [ 2015.1    2015.2    2015.3    2015.4  2015.45 ]
  // bezierCurve = [ x0,y0     x1,y1     x2,y2     x3,y3   x34,y34 ]
  // startIndex is 0 in scenario B
  // endIndex is 3 in scenario B
  //
  // scenario C: numDateMin = 2015.25, numDateMax = 2015.7
  // bezierDates = [ 2015.25  2015.3    2015.4    2015.5 ]
  // bezierCurve = [ x12,y12  x2,y2     x3,y3     x4,y4  ]
  // startIndex is 2 in scenario C
  // endIndex is 4 in scenario C
  //
  // scenario D: numDateMin = 2015.6, numDateMax = 2015.9
  // bezierDates = [ ]
  // bezierCurve = [ ]
  // startIndex is -1 in scenario D
  // endIndex is 4 in scenario D

  // find start walking forwards from left of array
  const startIndex = _findIndex(bezierDates, (d) => { return d > numDateMin; });

  // find end walking backwards from right of array
  const endIndex = _findLastIndex(bezierDates, (d) => { return d < numDateMax; });

  // startIndex and endIndex is -1 if not found
  // this indicates a slice of time that lies outside the bounds of BCurve
  // return empty array
  if (startIndex === -1 || endIndex === -1) {
    return [];
  }

  // get curve
  // slice takes index at begin
  // slice extracts up to but not including end
  const curve = bezierCurve.slice(startIndex, endIndex + 1);

  // if possible construct and prepend interpolated start
  let newStart;
  if (startIndex > 0) {
    // determine weighting of positions at startIndex and startIndex-1
    const dateDiff = bezierDates[startIndex] - bezierDates[startIndex - 1];
    const weightRight = (numDateMin - bezierDates[startIndex - 1]) / dateDiff;
    const weightLeft = (bezierDates[startIndex] - numDateMin) / dateDiff;
    // construct interpolated new start
    newStart = {
      x: (weightLeft * bezierCurve[startIndex - 1].x) + (weightRight * bezierCurve[startIndex].x),
      y: (weightLeft * bezierCurve[startIndex - 1].y) + (weightRight * bezierCurve[startIndex].y)
    };
    // will break indexing, so wait to prepend
  }

  // if possible construct and prepend interpolated start
  let newEnd;
  if (endIndex < bezierCurve.length - 1) {
    // determine weighting of positions at startIndex and startIndex-1
    const dateDiff = bezierDates[endIndex + 1] - bezierDates[endIndex];
    const weightRight = (numDateMax - bezierDates[endIndex]) / dateDiff;
    const weightLeft = (bezierDates[endIndex + 1] - numDateMax) / dateDiff;
    // construct interpolated new end
    newEnd = {
      x: (weightLeft * bezierCurve[endIndex].x) + (weightRight * bezierCurve[endIndex + 1].x),
      y: (weightLeft * bezierCurve[endIndex].y) + (weightRight * bezierCurve[endIndex + 1].y)
    };
    // will break indexing, so wait to append
  }

  // prepend / append interpolated points if they exist
  if (newStart) {
    curve.unshift(newStart);
  }
  if (newEnd) {
    curve.push(newEnd);
  }
  return curve;
};

const createArcsFromDemes = (demeData) => {
  const individualArcs = [];
  demeData.forEach((demeInfo) => {
    demeInfo.arcs.forEach((slice) => {
      individualArcs.push(slice);
    });
  });
  return individualArcs;
};

export const drawDemesAndTransmissions = (
  demeData,
  transmissionData,
  g,
  map,
  nodes,
  numDateMin,
  numDateMax,
  pieChart, /* bool */
  geoResolution,
  dispatch,
  showTransmissionLines
) => {

  // add transmission lines
  const transmissions = g.selectAll("transmissions")
    .data(transmissionData)
    .enter()
    .append("path") /* instead of appending a geodesic path from the leaflet plugin data, we now draw a line directly between two points */
    .attr("d", (d) => {
      return pathStringGenerator(
        extractLineSegmentForAnimationEffect(
          numDateMin,
          numDateMax,
          d.originCoords,
          d.destinationCoords,
          d.originNumDate,
          d.destinationNumDate,
          d.visible,
          d.bezierCurve,
          d.bezierDates
        )
      );
    })
    .attr("fill", "none")
    .attr("stroke-opacity", 0.6)
    .attr("stroke-linecap", "round")
    .attr("stroke", (d) => { return d.color; })
    .attr("stroke-width", showTransmissionLines ? 1 : 0);

  const visibleTips = nodes[0].tipCount;
  const demeMultiplier =
    demeCountMultiplier /
    Math.sqrt(_max([Math.sqrt(visibleTips * nodes.length), demeCountMinimum]));
  let demes;
  // determine whether to draw pieChart or not (sensible for categorical data)
  if (pieChart) {
    /* each datapoint in `demeData` contains `arcs` which comprise n individual "slices"
     * we need to create an array of all of these slices for d3 to render
     */
    const individualArcs = createArcsFromDemes(demeData);

    /* add `outerRadius` to all slices */
    // TODO - move this to initial arc creation in setupDemeData as it's only ever done once
    individualArcs.forEach((a) => {
      a.outerRadius = Math.sqrt(demeData[a.demeDataIdx].count)*demeMultiplier;
    });

    demes = g.selectAll('demes') // add individual arcs ("slices") to this selection
      .data(individualArcs)
      .enter().append("path")
      .attr("d", (d) => arc()(d))
      /* following calls are (almost) the same for pie charts & circles */
      .style("stroke", "none")
      .style("fill-opacity", 0.65)
      .style("fill", (d) => { return d.color; })
      .style("stroke-opacity", 0.85)
      .style("stroke", (d) => { return d.color; })
      .style("pointer-events", "all")
      .attr("transform", (d) =>
        "translate(" + demeData[d.demeDataIdx].coords.x + "," + demeData[d.demeDataIdx].coords.y + ")"
      )
      .on("mouseover", (d) => { dispatch(updateTipRadii({geoFilter: [geoResolution, demeData[d.demeDataIdx].name]})); })
      .on("mouseout", () => { dispatch(updateTipRadii()); });
  } else {
    demes = g.selectAll("demes") // add deme circles to this selection
      .data(demeData)
      .enter().append("circle")
      .attr("r", (d) => { return demeMultiplier * Math.sqrt(d.count); })
      /* following calls are (almost) the same for pie charts & circles */
      .style("stroke", "none")
      .style("fill-opacity", 0.65)
      .style("fill", (d) => { return d.color; })
      .style("stroke-opacity", 0.85)
      .style("stroke", (d) => { return d.color; })
      .style("pointer-events", "all")
      .attr("transform", (d) => "translate(" + d.coords.x + "," + d.coords.y + ")")
      .on("mouseover", (d) => { dispatch(updateTipRadii({geoFilter: [geoResolution, d.name]})); })
      .on("mouseout", () => { dispatch(updateTipRadii()); });
  }

  return {
    demes,
    transmissions
  };

};

export const updateOnMoveEnd = (demeData, transmissionData, d3elems, numDateMin, numDateMax, pieChart) => {
  /* map has moved or rescaled, make demes and transmissions line up */
  if (!d3elems) { return; }

  /* move the pie charts differently to the color-blended circles */
  if (pieChart) {
    const individualArcs = createArcsFromDemes(demeData);
    d3elems.demes
      .data(individualArcs)
      .attr("transform", (d) =>
        /* copied from above. TODO. */
        "translate(" + demeData[d.demeDataIdx].coords.x + "," + demeData[d.demeDataIdx].coords.y + ")"
      );
  } else {
    d3elems.demes
      .data(demeData)
      .attr("transform", (d) =>
        /* copied from above. TODO. */
        "translate(" + d.coords.x + "," + d.coords.y + ")"
      );
  }

  d3elems.transmissions
    .data(transmissionData)
    .attr("d", (d) => {
      return pathStringGenerator(
        extractLineSegmentForAnimationEffect(
          numDateMin,
          numDateMax,
          d.originCoords,
          d.destinationCoords,
          d.originNumDate,
          d.destinationNumDate,
          d.visible,
          d.bezierCurve,
          d.bezierDates
        )
      );
    }); // other attrs remain the same as they were
};

export const updateVisibility = (
  demeData,
  transmissionData,
  d3elems,
  map,
  nodes,
  numDateMin,
  numDateMax,
  pieChart
) => {

  if (!d3elems) {
    console.error("d3elems is not defined!");
    return;
  }
  const visibleTips = nodes[0].tipCount;
  const demeMultiplier =
    demeCountMultiplier /
    Math.sqrt(_max([Math.sqrt(visibleTips * nodes.length), demeCountMinimum]));

  if (pieChart) {
    const individualArcs = createArcsFromDemes(demeData);
    /* add `outerRadius` to all slices */
    // TODO - move this to initial arc creation in setupDemeData as it's only ever done once
    individualArcs.forEach((a) => {
      a.outerRadius = Math.sqrt(demeData[a.demeDataIdx].count)*demeMultiplier;
    });
    d3elems.demes
      .data(individualArcs)
      .attr("d", (d) => arc()(d))
      .style("fill", (d) => { return d.color; })
      .style("stroke", (d) => { return d.color; });
  } else {
    /* for colour blended circles we just have to update the colours & size (radius) */
    d3elems.demes
      .data(demeData)
      .transition()
      .duration(200)
      .ease(easeLinear)
      .style("stroke", (d) => { return d.count > 0 ? d.color : "white"; })
      .style("fill", (d) => { return d.count > 0 ? d.color : "white"; })
      .attr("r", (d) => { return demeMultiplier * Math.sqrt(d.count); });
  }

  /* update the path and stroke colour of transmission lines */
  d3elems.transmissions
    .data(transmissionData)
    .attr("d", (d) => {
      return pathStringGenerator(
        extractLineSegmentForAnimationEffect(
          numDateMin,
          numDateMax,
          d.originCoords,
          d.destinationCoords,
          d.originNumDate,
          d.destinationNumDate,
          d.visible,
          d.bezierCurve,
          d.bezierDates
        )
      );
    }) /* with the interpolation in the function above pathStringGenerator */
    .attr("stroke", (d) => { return d.color; });
};

/* template for an update helper */
export const updateFoo = (d3elems, latLongs) => {
  d3elems.demes
    .data(latLongs.demes);

  d3elems.transmissions
    .data(latLongs.transmissions);
};

/*
  http://gis.stackexchange.com/questions/49114/d3-geo-path-to-draw-a-path-from-gis-coordinates
  https://bl.ocks.org/mbostock/3916621  // Compute point-interpolators at each distance.
  http://bl.ocks.org/mbostock/5851933 // draw line on map
  https://gist.github.com/mikeatlas/0b69b354a8d713989147 // polyline split if we don't use leaflet
  http://bl.ocks.org/mbostock/5928813
  http://bl.ocks.org/duopixel/4063326 // animate path in
  https://bl.ocks.org/mbostock/1705868 // point along path interpolation
  https://bl.ocks.org/mbostock/1313857 // point along path interpolation
  https://github.com/d3/d3-shape/blob/master/README.md#curves
  https://github.com/d3/d3-shape/blob/master/README.md#lines
*/
<|endoftext|>


<commit_before>/*
 * Helpers to manage action on configuration changes.
 */
var configWatcher = function (path){

  var pW = {

    mainWatcher: null,
    watchers: [],

    trigger: function () {
      this.watchers.forEach(function (watcher) {
        watcher();
      });
    },

    on: function (newWatcher) {
      var index = this.watchers.indexOf(newWatcher);
      if (index === -1) { this.watchers.push(newWatcher); }
    },

    off: function (watcher) {
      this.watchers.splice(this.watchers.indexOf(watcher), 1);
    },

    one: function (watcher) {
      var oner = function () {
        configWatcher.off(oner);
        if (watcher !== undefined && typeof(watcher) === 'function') {
          watcher();
        }
      };
      configWatcher.on(oner);
    }
  };

  var options = {persistent: false, interval: 1000};
  pW.mainWatcher = fs.watchFile(
    path, options, pW.trigger
  );

  return pW;
};
module.exports = configWatcher;<commit_msg>variable rename<commit_after>/*
 * Helpers to manage action on configuration changes.
 */

var fs = require('fs-extra');

var configWatcher = function (path){

  var pW = {

    mainWatcher: null,
    watchers: [],

    trigger: function () {
      pW.watchers.forEach(function (watcher) {
        watcher();
      });
    },

    on: function (newWatcher) {
      var index = pW.watchers.indexOf(newWatcher);
      if (index === -1) { pW.watchers.push(newWatcher); }
    },

    off: function (watcher) {
      this.watchers.splice(pW.watchers.indexOf(watcher), 1);
    },

    one: function (watcher) {
      var oner = function () {
        pW.off(oner);
        if (watcher !== undefined && typeof(watcher) === 'function') {
          watcher();
        }
      };
      pW.on(oner);
    }
  };

  var options = {persistent: false, interval: 1000};
  pW.mainWatcher = fs.watchFile(
    path, options, pW.trigger
  );

  return pW;
};
module.exports = configWatcher;<|endoftext|>