<commit_before>/* @flow */

import * as React from 'react';
import { StyleSheet, Keyboard } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, { Easing } from 'react-native-reanimated';

import type {
  Layout,
  NavigationState,
  Route,
  Listener,
  PagerCommonProps,
} from './types';

type Props<T: Route> = {|
  ...PagerCommonProps,
  onIndexChange: (index: number) => mixed,
  navigationState: NavigationState<T>,
  layout: Layout,
  // Clip unfocused views to improve memory usage
  // Don't enable this on iOS where this is buggy and views don't re-appear
  removeClippedSubviews?: boolean,
  children: (props: {|
    // Animated value which represents the state of current index
    // It can include fractional digits as it represents the intermediate value
    position: Animated.Node<number>,
    // Function to actually render the content of the pager
    // The parent component takes care of rendering
    render: (children: React.Node) => React.Node,
    // Add a listener to listen for position updates
    addListener: (type: 'position', listener: Listener) => void,
    // Remove a position listener
    removeListener: (type: 'position', listener: Listener) => void,
    // Callback to call when switching the tab
    // The tab switch animation is performed even if the index in state is unchanged
    jumpTo: (key: string) => void,
  |}) => React.Node,
|};

const {
  Clock,
  Value,
  onChange,
  abs,
  add,
  and,
  block,
  call,
  clockRunning,
  cond,
  divide,
  eq,
  event,
  greaterThan,
  max,
  min,
  multiply,
  neq,
  or,
  round,
  set,
  spring,
  startClock,
  stopClock,
  sub,
  timing,
} = Animated;

const TRUE = 1;
const FALSE = 0;
const NOOP = 0;
const UNSET = -1;

const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;

const SWIPE_DISTANCE_MINIMUM = 20;
const SWIPE_DISTANCE_MULTIPLIER = 1 / 1.75;

const SPRING_CONFIG = {
  damping: 30,
  mass: 1,
  stiffness: 200,
  overshootClamping: true,
  restSpeedThreshold: 0.001,
  restDisplacementThreshold: 0.001,
};

const TIMING_CONFIG = {
  duration: 250,
  easing: Easing.out(Easing.cubic),
};

export default class Pager<T: Route> extends React.Component<Props<T>> {
  static defaultProps = {
    swipeVelocityThreshold: 1200,
  };

  componentDidUpdate(prevProps: Props<T>) {
    const { index } = this.props.navigationState;

    if (
      // Check for index in state to avoid unintended transition if component updates during swipe
      (index !== prevProps.navigationState.index &&
        index !== this._currentIndexValue) ||
      // Check if the user updated the index correctly after an update
      (typeof this._pendingIndexValue === 'number' &&
        index !== this._pendingIndexValue)
    ) {
      // Index in user's state is different from the index being tracked
      this._jumpToIndex(index);
    }

    // Reset the pending index
    this._pendingIndexValue = undefined;

    // Update our mappings of animated nodes when props change
    if (
      prevProps.navigationState.routes.length !==
      this.props.navigationState.routes.length
    ) {
      this._routesLength.setValue(this.props.navigationState.routes.length);
    }

    if (prevProps.layout.width !== this.props.layout.width) {
      this._position.setValue(-index * this.props.layout.width);
      this._layoutWidth.setValue(this.props.layout.width);
    }

    if (this.props.swipeDistanceThreshold != null) {
      if (
        prevProps.swipeDistanceThreshold !== this.props.swipeDistanceThreshold
      ) {
        this._swipeDistanceThreshold.setValue(
          this.props.swipeDistanceThreshold
        );
      }
    } else {
      if (prevProps.layout.width !== this.props.layout.width) {
        this._swipeDistanceThreshold.setValue(
          this.props.layout.width * SWIPE_DISTANCE_MULTIPLIER
        );
      }
    }

    if (
      prevProps.swipeVelocityThreshold !== this.props.swipeVelocityThreshold
    ) {
      this._swipeVelocityThreshold.setValue(this.props.swipeVelocityThreshold);
    }
  }

  // Clock used for tab transition animations
  _clock = new Clock();

  // Current state of the gesture
  _velocityX = new Value(0);
  _gestureX = new Value(0);
  _gestureState = new Value(State.UNDETERMINED);
  _offsetX = new Value(0);

  // Current position of the page (translateX value)
  _position = new Value(
    // Intial value is based on the index and page width
    this.props.navigationState.index * this.props.layout.width * DIRECTION_RIGHT
  );

  // Initial index of the tabs
  _index = new Value(this.props.navigationState.index);

  // Next index of the tabs, updated for navigation from outside (tab press, state update)
  _nextIndex = new Value(UNSET);

  // Whether the user is currently dragging the screen
  _isSwiping = new Value(FALSE);

  // Whether the update was due to swipe gesture
  // This controls whether the transition will use a spring or timing animation
  // Remember to set it before transition needs to occur
  _isSwipeGesture = new Value(FALSE);

  // Mappings to some prop values
  // We use them in animation calculations, so we need live animated nodes
  _routesLength = new Value(this.props.navigationState.routes.length);
  _layoutWidth = new Value(this.props.layout.width);

  // Threshold values to determine when to trigger a swipe gesture
  _swipeDistanceThreshold = new Value(this.props.swipeDistanceThreshold || 180);
  _swipeVelocityThreshold = new Value(this.props.swipeVelocityThreshold);

  // Whether we need to add a listener for position change
  // To avoid unnecessary traffic through the bridge, don't add listeners unless needed
  _isListening = new Value(FALSE);

  // The current index change caused by the pager's animation
  // The pager is used as a controlled component
  // We need to keep track of the index to determine when to trigger animation
  // The state will change at various points, we should only respond when we are out of sync
  // This will ensure smoother animation and avoid weird glitches
  _currentIndexValue = this.props.navigationState.index;

  // The pending index value as result of state update caused by swipe gesture
  // We need to set it when state changes from inside this component
  // It also needs to be reset right after componentDidUpdate fires
  _pendingIndexValue: ?number = undefined;

  // Listeners for the animated value
  _positionListeners: Listener[] = [];

  _jumpToIndex = (index: number) => {
    // If the index changed, we need to trigger a tab switch
    this._isSwipeGesture.setValue(FALSE);
    this._nextIndex.setValue(index);
  };

  _jumpTo = (key: string) => {
    const { navigationState } = this.props;

    const index = navigationState.routes.findIndex(route => route.key === key);

    // A tab switch might occur when we're in the middle of a transition
    // In that case, the index might be same as before
    // So we conditionally make the pager to update the position
    if (navigationState.index === index) {
      this._jumpToIndex(index);
    } else {
      this.props.onIndexChange(index);
    }
  };

  _addListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    this._positionListeners.push(listener);
    this._isListening.setValue(TRUE);
  };

  _removeListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    const index = this._positionListeners.indexOf(listener);

    if (index > -1) {
      this._positionListeners.splice(index, 1);
    }

    if (this._positionListeners.length === 0) {
      this._isListening.setValue(FALSE);
    }
  };

  _handlePositionChange = ([translateX]: [number]) => {
    // The position value is calculated based on the translate value
    // If we don't have the layout yet, we should return the current index
    const value = this.props.layout.width
      ? Math.abs(translateX / this.props.layout.width)
      : this.props.navigationState.index;

    this._positionListeners.forEach(listener => listener(value));
  };

  _transitionTo = (index: Animated.Node<number>) => {
    const toValue = new Value(0);
    const frameTime = new Value(0);

    const state = {
      position: this._position,
      time: new Value(0),
      finished: new Value(FALSE),
    };

    return block([
      cond(clockRunning(this._clock), NOOP, [
        // Animation wasn't running before
        // Set the initial values and start the clock
        set(toValue, multiply(index, this._layoutWidth, DIRECTION_RIGHT)),
        set(frameTime, 0),
        set(state.time, 0),
        set(state.finished, FALSE),
        set(this._index, index),
        startClock(this._clock),
      ]),
      cond(
        this._isSwipeGesture,
        // Animate the values with a spring for swipe
        spring(
          this._clock,
          { ...state, velocity: this._velocityX },
          { ...SPRING_CONFIG, toValue }
        ),
        // Otherwise use a timing animation for faster switching
        timing(
          this._clock,
          { ...state, frameTime },
          { ...TIMING_CONFIG, toValue }
        )
      ),
      cond(state.finished, [
        // Reset gesture and velocity from previous gesture
        set(this._gestureX, 0),
        set(this._velocityX, 0),
        // When the animation finishes, stop the clock
        stopClock(this._clock),
        call([this._index], ([value]) => {
          // If the index changed, and previous animation has finished, update state
          this.props.onIndexChange(value);

          // Without this check, the pager can go to an infinite update <-> animate loop for sync updates
          if (value !== this.props.navigationState.index) {
            this._pendingIndexValue = value;

            // Force componentDidUpdate to fire, whether user does a setState or not
            // This allows us to detect when the user drops the update and revert back
            // It's necessary to make sure that the state stays in sync
            this.forceUpdate();
          }
        }),
      ]),
    ]);
  };

  _handleGestureEvent = event([
    {
      nativeEvent: {
        translationX: this._gestureX,
        velocityX: this._velocityX,
        state: this._gestureState,
      },
    },
  ]);

  _translateX = block([
    call([this._index], ([value]) => {
      this._currentIndexValue = value;
    }),
    // Conditionally listen for changes in the position value
    // The position value can changes a lot in short time
    // So we only add a listener when necessary to avoid extra overhead
    cond(
      this._isListening,
      onChange(
        this._position,
        call([this._position], this._handlePositionChange)
      )
    ),
    onChange(
      this._isSwiping,
      // Listen to updates for this value only when it changes
      // Without `onChange`, this will fire even if the value didn't change
      // We don't want to call the listeners if the value didn't change
      call([this._isSwiping], ([value]) => {
        const { keyboardDismissMode, onSwipeStart, onSwipeEnd } = this.props;

        if (value === TRUE) {
          onSwipeStart && onSwipeStart();

          if (keyboardDismissMode === 'on-drag') {
            Keyboard.dismiss();
          }
        } else {
          onSwipeEnd && onSwipeEnd();
        }
      })
    ),
    onChange(
      this._nextIndex,
      cond(neq(this._nextIndex, UNSET), [
        // Stop any running animations
        cond(clockRunning(this._clock), stopClock(this._clock)),
        // Update the index to trigger the transition
        set(this._index, this._nextIndex),
        set(this._nextIndex, UNSET),
      ])
    ),
    cond(
      eq(this._gestureState, State.ACTIVE),
      [
        cond(this._isSwiping, NOOP, [
          // We weren't dragging before, set it to true
          set(this._isSwiping, TRUE),
          set(this._isSwipeGesture, TRUE),
          // Also update the drag offset to the last position
          set(this._offsetX, this._position),
        ]),
        // Update position with previous offset + gesture distance
        set(this._position, add(this._offsetX, this._gestureX)),
        // Stop animations while we're dragging
        stopClock(this._clock),
      ],
      [
        set(this._isSwiping, FALSE),
        this._transitionTo(
          cond(
            and(
              greaterThan(abs(this._gestureX), SWIPE_DISTANCE_MINIMUM),
              or(
                greaterThan(abs(this._gestureX), this._swipeDistanceThreshold),
                greaterThan(abs(this._velocityX), this._swipeVelocityThreshold)
              )
            ),
            // For swipe gesture, to calculate the index, determine direction and add to index
            // When the user swipes towards the left, we transition to the next tab
            // When the user swipes towards the right, we transition to the previous tab
            round(
              min(
                max(
                  0,
                  sub(
                    this._index,
                    cond(
                      greaterThan(
                        // Gesture can be positive, or negative
                        // Get absolute for comparision
                        abs(this._gestureX),
                        this._swipeDistanceThreshold
                      ),
                      // If gesture value exceeded the threshold, calculate direction from distance travelled
                      cond(
                        greaterThan(this._gestureX, 0),
                        DIRECTION_LEFT,
                        DIRECTION_RIGHT
                      ),
                      // Otherwise calculate direction from the gesture velocity
                      cond(
                        greaterThan(this._velocityX, 0),
                        DIRECTION_LEFT,
                        DIRECTION_RIGHT
                      )
                    )
                  )
                ),
                sub(this._routesLength, 1)
              )
            ),
            // Index didn't change/changed due to state update
            this._index
          )
        ),
      ]
    ),
    this._position,
  ]);

  render() {
    const {
      layout,
      navigationState,
      swipeEnabled,
      children,
      removeClippedSubviews,
    } = this.props;

    // Make sure that the translation doesn't exceed the bounds to prevent overscrolling
    const translateX = min(
      max(
        multiply(
          this._layoutWidth,
          sub(this._routesLength, 1),
          DIRECTION_RIGHT
        ),
        this._translateX
      ),
      0
    );

    const position = cond(
      this._layoutWidth,
      divide(abs(translateX), this._layoutWidth),
      this._index
    );

    return children({
      position,
      addListener: this._addListener,
      removeListener: this._removeListener,
      jumpTo: this._jumpTo,
      render: children => (
        <PanGestureHandler
          enabled={layout.width !== 0 && swipeEnabled}
          onGestureEvent={this._handleGestureEvent}
          onHandlerStateChange={this._handleGestureEvent}
          activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
          failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
        >
          <Animated.View
            removeClippedSubviews={removeClippedSubviews}
            style={[
              styles.container,
              layout.width
                ? {
                    width: layout.width * navigationState.routes.length,
                    transform: [{ translateX }],
                  }
                : null,
            ]}
          >
            {children}
          </Animated.View>
        </PanGestureHandler>
      ),
    });
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
});
<commit_msg>fix: reset _isSwipeGesture when state.finished (#719) (#730)<commit_after>/* @flow */

import * as React from 'react';
import { StyleSheet, Keyboard } from 'react-native';
import { PanGestureHandler, State } from 'react-native-gesture-handler';
import Animated, { Easing } from 'react-native-reanimated';

import type {
  Layout,
  NavigationState,
  Route,
  Listener,
  PagerCommonProps,
} from './types';

type Props<T: Route> = {|
  ...PagerCommonProps,
  onIndexChange: (index: number) => mixed,
  navigationState: NavigationState<T>,
  layout: Layout,
  // Clip unfocused views to improve memory usage
  // Don't enable this on iOS where this is buggy and views don't re-appear
  removeClippedSubviews?: boolean,
  children: (props: {|
    // Animated value which represents the state of current index
    // It can include fractional digits as it represents the intermediate value
    position: Animated.Node<number>,
    // Function to actually render the content of the pager
    // The parent component takes care of rendering
    render: (children: React.Node) => React.Node,
    // Add a listener to listen for position updates
    addListener: (type: 'position', listener: Listener) => void,
    // Remove a position listener
    removeListener: (type: 'position', listener: Listener) => void,
    // Callback to call when switching the tab
    // The tab switch animation is performed even if the index in state is unchanged
    jumpTo: (key: string) => void,
  |}) => React.Node,
|};

const {
  Clock,
  Value,
  onChange,
  abs,
  add,
  and,
  block,
  call,
  clockRunning,
  cond,
  divide,
  eq,
  event,
  greaterThan,
  max,
  min,
  multiply,
  neq,
  or,
  round,
  set,
  spring,
  startClock,
  stopClock,
  sub,
  timing,
} = Animated;

const TRUE = 1;
const FALSE = 0;
const NOOP = 0;
const UNSET = -1;

const DIRECTION_LEFT = 1;
const DIRECTION_RIGHT = -1;

const SWIPE_DISTANCE_MINIMUM = 20;
const SWIPE_DISTANCE_MULTIPLIER = 1 / 1.75;

const SPRING_CONFIG = {
  damping: 30,
  mass: 1,
  stiffness: 200,
  overshootClamping: true,
  restSpeedThreshold: 0.001,
  restDisplacementThreshold: 0.001,
};

const TIMING_CONFIG = {
  duration: 250,
  easing: Easing.out(Easing.cubic),
};

export default class Pager<T: Route> extends React.Component<Props<T>> {
  static defaultProps = {
    swipeVelocityThreshold: 1200,
  };

  componentDidUpdate(prevProps: Props<T>) {
    const { index } = this.props.navigationState;

    if (
      // Check for index in state to avoid unintended transition if component updates during swipe
      (index !== prevProps.navigationState.index &&
        index !== this._currentIndexValue) ||
      // Check if the user updated the index correctly after an update
      (typeof this._pendingIndexValue === 'number' &&
        index !== this._pendingIndexValue)
    ) {
      // Index in user's state is different from the index being tracked
      this._jumpToIndex(index);
    }

    // Reset the pending index
    this._pendingIndexValue = undefined;

    // Update our mappings of animated nodes when props change
    if (
      prevProps.navigationState.routes.length !==
      this.props.navigationState.routes.length
    ) {
      this._routesLength.setValue(this.props.navigationState.routes.length);
    }

    if (prevProps.layout.width !== this.props.layout.width) {
      this._position.setValue(-index * this.props.layout.width);
      this._layoutWidth.setValue(this.props.layout.width);
    }

    if (this.props.swipeDistanceThreshold != null) {
      if (
        prevProps.swipeDistanceThreshold !== this.props.swipeDistanceThreshold
      ) {
        this._swipeDistanceThreshold.setValue(
          this.props.swipeDistanceThreshold
        );
      }
    } else {
      if (prevProps.layout.width !== this.props.layout.width) {
        this._swipeDistanceThreshold.setValue(
          this.props.layout.width * SWIPE_DISTANCE_MULTIPLIER
        );
      }
    }

    if (
      prevProps.swipeVelocityThreshold !== this.props.swipeVelocityThreshold
    ) {
      this._swipeVelocityThreshold.setValue(this.props.swipeVelocityThreshold);
    }
  }

  // Clock used for tab transition animations
  _clock = new Clock();

  // Current state of the gesture
  _velocityX = new Value(0);
  _gestureX = new Value(0);
  _gestureState = new Value(State.UNDETERMINED);
  _offsetX = new Value(0);

  // Current position of the page (translateX value)
  _position = new Value(
    // Intial value is based on the index and page width
    this.props.navigationState.index * this.props.layout.width * DIRECTION_RIGHT
  );

  // Initial index of the tabs
  _index = new Value(this.props.navigationState.index);

  // Next index of the tabs, updated for navigation from outside (tab press, state update)
  _nextIndex = new Value(UNSET);

  // Whether the user is currently dragging the screen
  _isSwiping = new Value(FALSE);

  // Whether the update was due to swipe gesture
  // This controls whether the transition will use a spring or timing animation
  // Remember to set it before transition needs to occur
  _isSwipeGesture = new Value(FALSE);

  // Mappings to some prop values
  // We use them in animation calculations, so we need live animated nodes
  _routesLength = new Value(this.props.navigationState.routes.length);
  _layoutWidth = new Value(this.props.layout.width);

  // Threshold values to determine when to trigger a swipe gesture
  _swipeDistanceThreshold = new Value(this.props.swipeDistanceThreshold || 180);
  _swipeVelocityThreshold = new Value(this.props.swipeVelocityThreshold);

  // Whether we need to add a listener for position change
  // To avoid unnecessary traffic through the bridge, don't add listeners unless needed
  _isListening = new Value(FALSE);

  // The current index change caused by the pager's animation
  // The pager is used as a controlled component
  // We need to keep track of the index to determine when to trigger animation
  // The state will change at various points, we should only respond when we are out of sync
  // This will ensure smoother animation and avoid weird glitches
  _currentIndexValue = this.props.navigationState.index;

  // The pending index value as result of state update caused by swipe gesture
  // We need to set it when state changes from inside this component
  // It also needs to be reset right after componentDidUpdate fires
  _pendingIndexValue: ?number = undefined;

  // Listeners for the animated value
  _positionListeners: Listener[] = [];

  _jumpToIndex = (index: number) => {
    // If the index changed, we need to trigger a tab switch
    this._isSwipeGesture.setValue(FALSE);
    this._nextIndex.setValue(index);
  };

  _jumpTo = (key: string) => {
    const { navigationState } = this.props;

    const index = navigationState.routes.findIndex(route => route.key === key);

    // A tab switch might occur when we're in the middle of a transition
    // In that case, the index might be same as before
    // So we conditionally make the pager to update the position
    if (navigationState.index === index) {
      this._jumpToIndex(index);
    } else {
      this.props.onIndexChange(index);
    }
  };

  _addListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    this._positionListeners.push(listener);
    this._isListening.setValue(TRUE);
  };

  _removeListener = (type: string, listener: Listener) => {
    if (type !== 'position') {
      return;
    }

    const index = this._positionListeners.indexOf(listener);

    if (index > -1) {
      this._positionListeners.splice(index, 1);
    }

    if (this._positionListeners.length === 0) {
      this._isListening.setValue(FALSE);
    }
  };

  _handlePositionChange = ([translateX]: [number]) => {
    // The position value is calculated based on the translate value
    // If we don't have the layout yet, we should return the current index
    const value = this.props.layout.width
      ? Math.abs(translateX / this.props.layout.width)
      : this.props.navigationState.index;

    this._positionListeners.forEach(listener => listener(value));
  };

  _transitionTo = (index: Animated.Node<number>) => {
    const toValue = new Value(0);
    const frameTime = new Value(0);

    const state = {
      position: this._position,
      time: new Value(0),
      finished: new Value(FALSE),
    };

    return block([
      cond(clockRunning(this._clock), NOOP, [
        // Animation wasn't running before
        // Set the initial values and start the clock
        set(toValue, multiply(index, this._layoutWidth, DIRECTION_RIGHT)),
        set(frameTime, 0),
        set(state.time, 0),
        set(state.finished, FALSE),
        set(this._index, index),
        startClock(this._clock),
      ]),
      cond(
        this._isSwipeGesture,
        // Animate the values with a spring for swipe
        spring(
          this._clock,
          { ...state, velocity: this._velocityX },
          { ...SPRING_CONFIG, toValue }
        ),
        // Otherwise use a timing animation for faster switching
        timing(
          this._clock,
          { ...state, frameTime },
          { ...TIMING_CONFIG, toValue }
        )
      ),
      cond(state.finished, [
        // Reset values
        set(this._isSwipeGesture, FALSE),
        set(this._gestureX, 0),
        set(this._velocityX, 0),
        // When the animation finishes, stop the clock
        stopClock(this._clock),
        call([this._index], ([value]) => {
          // If the index changed, and previous animation has finished, update state
          this.props.onIndexChange(value);

          // Without this check, the pager can go to an infinite update <-> animate loop for sync updates
          if (value !== this.props.navigationState.index) {
            this._pendingIndexValue = value;

            // Force componentDidUpdate to fire, whether user does a setState or not
            // This allows us to detect when the user drops the update and revert back
            // It's necessary to make sure that the state stays in sync
            this.forceUpdate();
          }
        }),
      ]),
    ]);
  };

  _handleGestureEvent = event([
    {
      nativeEvent: {
        translationX: this._gestureX,
        velocityX: this._velocityX,
        state: this._gestureState,
      },
    },
  ]);

  _translateX = block([
    call([this._index], ([value]) => {
      this._currentIndexValue = value;
    }),
    // Conditionally listen for changes in the position value
    // The position value can changes a lot in short time
    // So we only add a listener when necessary to avoid extra overhead
    cond(
      this._isListening,
      onChange(
        this._position,
        call([this._position], this._handlePositionChange)
      )
    ),
    onChange(
      this._isSwiping,
      // Listen to updates for this value only when it changes
      // Without `onChange`, this will fire even if the value didn't change
      // We don't want to call the listeners if the value didn't change
      call([this._isSwiping], ([value]) => {
        const { keyboardDismissMode, onSwipeStart, onSwipeEnd } = this.props;

        if (value === TRUE) {
          onSwipeStart && onSwipeStart();

          if (keyboardDismissMode === 'on-drag') {
            Keyboard.dismiss();
          }
        } else {
          onSwipeEnd && onSwipeEnd();
        }
      })
    ),
    onChange(
      this._nextIndex,
      cond(neq(this._nextIndex, UNSET), [
        // Stop any running animations
        cond(clockRunning(this._clock), stopClock(this._clock)),
        // Update the index to trigger the transition
        set(this._index, this._nextIndex),
        set(this._nextIndex, UNSET),
      ])
    ),
    cond(
      eq(this._gestureState, State.ACTIVE),
      [
        cond(this._isSwiping, NOOP, [
          // We weren't dragging before, set it to true
          set(this._isSwiping, TRUE),
          set(this._isSwipeGesture, TRUE),
          // Also update the drag offset to the last position
          set(this._offsetX, this._position),
        ]),
        // Update position with previous offset + gesture distance
        set(this._position, add(this._offsetX, this._gestureX)),
        // Stop animations while we're dragging
        stopClock(this._clock),
      ],
      [
        set(this._isSwiping, FALSE),
        this._transitionTo(
          cond(
            and(
              greaterThan(abs(this._gestureX), SWIPE_DISTANCE_MINIMUM),
              or(
                greaterThan(abs(this._gestureX), this._swipeDistanceThreshold),
                greaterThan(abs(this._velocityX), this._swipeVelocityThreshold)
              )
            ),
            // For swipe gesture, to calculate the index, determine direction and add to index
            // When the user swipes towards the left, we transition to the next tab
            // When the user swipes towards the right, we transition to the previous tab
            round(
              min(
                max(
                  0,
                  sub(
                    this._index,
                    cond(
                      greaterThan(
                        // Gesture can be positive, or negative
                        // Get absolute for comparision
                        abs(this._gestureX),
                        this._swipeDistanceThreshold
                      ),
                      // If gesture value exceeded the threshold, calculate direction from distance travelled
                      cond(
                        greaterThan(this._gestureX, 0),
                        DIRECTION_LEFT,
                        DIRECTION_RIGHT
                      ),
                      // Otherwise calculate direction from the gesture velocity
                      cond(
                        greaterThan(this._velocityX, 0),
                        DIRECTION_LEFT,
                        DIRECTION_RIGHT
                      )
                    )
                  )
                ),
                sub(this._routesLength, 1)
              )
            ),
            // Index didn't change/changed due to state update
            this._index
          )
        ),
      ]
    ),
    this._position,
  ]);

  render() {
    const {
      layout,
      navigationState,
      swipeEnabled,
      children,
      removeClippedSubviews,
    } = this.props;

    // Make sure that the translation doesn't exceed the bounds to prevent overscrolling
    const translateX = min(
      max(
        multiply(
          this._layoutWidth,
          sub(this._routesLength, 1),
          DIRECTION_RIGHT
        ),
        this._translateX
      ),
      0
    );

    const position = cond(
      this._layoutWidth,
      divide(abs(translateX), this._layoutWidth),
      this._index
    );

    return children({
      position,
      addListener: this._addListener,
      removeListener: this._removeListener,
      jumpTo: this._jumpTo,
      render: children => (
        <PanGestureHandler
          enabled={layout.width !== 0 && swipeEnabled}
          onGestureEvent={this._handleGestureEvent}
          onHandlerStateChange={this._handleGestureEvent}
          activeOffsetX={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
          failOffsetY={[-SWIPE_DISTANCE_MINIMUM, SWIPE_DISTANCE_MINIMUM]}
        >
          <Animated.View
            removeClippedSubviews={removeClippedSubviews}
            style={[
              styles.container,
              layout.width
                ? {
                    width: layout.width * navigationState.routes.length,
                    transform: [{ translateX }],
                  }
                : null,
            ]}
          >
            {children}
          </Animated.View>
        </PanGestureHandler>
      ),
    });
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    flexDirection: 'row',
  },
});
<|endoftext|>


<commit_before>#!/usr/bin/env node

/**
 * Cinema4D commandline Render Utilities
 * Rendering projects via terminal. This is useful for automation...
 *
 * @author: Paul Vollmer
 */

/**
 * Module dependencies.
 */
var program = require('commander');
var config = require('./config');
var pkg = require('../package.json');
var utils = require('../src/utils.js');
var c4d = require('../src/index.js');

/**
 * Print out the CINEMA 4D -help.
 * Only used for developing and debugging this tool.
 */
// var c4dHelp = spawn(config.cinema4d_path, ['-help']);
// c4dHelp.stdout.on('data', function (data) {
//     console.log(data.toString());
// });
// c4dHelp.on('close', function (code) {
//     console.log('process exit code ' + code);
// });

/**
 * The cli options.
 */
program
  .version(pkg.version)
  .option('-r, --render [filepath]', 'Render the added file.')
  .option('-s, --silent', 'silent mode. don\'t output anything')
program.on('--help', function() {
  console.log('  Description:');
  console.log('');
  console.log('    '+pkg.description);
  console.log('');
  console.log('  Examples:');
  console.log('');
  console.log('    # render a file');
  console.log('    $ c4d -r /path/to/your/file.c4d');
  console.log('');
});
program.parse(process.argv);

/**
 * If no option are set. print out the help.
 */
if (program.render == undefined) {
  program.help();
}
/**
 * If the render option is set, lets render some nice images.
 */
else {
  if (program.render) {
    c4d.render({
      silent: program.silent,
      file: program.render,
      out_format: 'TIFF'
    });
  }
}
<commit_msg>bin/c4d-cli.js delete dead code<commit_after>#!/usr/bin/env node

/**
 * Cinema4D commandline Render Utilities
 * Rendering projects via terminal. This is useful for automation...
 *
 * @author: Paul Vollmer
 */

/**
 * Module dependencies.
 */
var program = require('commander');
var config = require('./config');
var pkg = require('../package.json');
var utils = require('../src/utils.js');
var c4d = require('../src/index.js');

/**
 * The cli options.
 */
program
  .version(pkg.version)
  .option('-r, --render [filepath]', 'Render the added file.')
  .option('-s, --silent', 'silent mode. don\'t output anything')
program.on('--help', function() {
  console.log('  Description:');
  console.log('');
  console.log('    '+pkg.description);
  console.log('');
  console.log('  Examples:');
  console.log('');
  console.log('    # render a file');
  console.log('    $ c4d -r /path/to/your/file.c4d');
  console.log('');
});
program.parse(process.argv);

/**
 * If no option are set. print out the help.
 */
if (program.render == undefined) {
  program.help();
}
/**
 * If the render option is set, lets render some nice images.
 */
else {
  if (program.render) {
    c4d.render({
      silent: program.silent,
      file: program.render,
      out_format: 'TIFF'
    });
  }
}
<|endoftext|>


<commit_before>define(['react', 'models/application', 'collections/applications',
'components/images/cards', 'components/common/modal', 'jquery',
'components/common/time'], function(React, App, AppCollection, Cards, Modal, $,
Time) {

    var Machine = React.createClass({
        render: function() {
            var machine = this.props.machine;
            console.log(machine);
            return React.DOM.li({}, "Version: ", machine.get('pretty_version'),
                React.DOM.br(),
                "Date: ", Time({date: machine.get('start_date'), showRelative: false}));
        }
    });

    var MachineList = React.createClass({
        render: function() {
            var versions = this.props.machines.map(function(model) {
                return Machine({key: model.id, machine: model});
            });
            return React.DOM.ul({}, versions);
        }
    });

    var LaunchApplicationModal = React.createClass({
        render: function() {
            return Modal({id: 'launch-modal', title: 'Launch Instance'},
                React.DOM.div({className: 'modal-body'},
                    React.DOM.span({}, "hello")),
                React.DOM.div({className: 'modal-footer'},
                    React.DOM.span({}, "hello")));
        }
    });

    var ApplicationDetail = React.createClass({
        getInitialState: function() {
            return {application: null};
        },
        componentDidMount: function() {
            // TODO: This is what should happen if we have API support for it
            /*
            var app = new App({id: this.props.image_id});
            app.fetch({success: function(model) {
                this.setState({image: model});
            }.bind(this)});
            */
            // This is terrible.
            var apps = new AppCollection();
            apps.fetch({success: function(collection) {
                var app = collection.get(this.props.image_id);
                this.setState({application: app});
            }.bind(this)});
        },
        showModal: function(e) {
            $('#launch-modal').modal('show');
        },
        render: function() {
            var app = this.state.application;

            if (!app)
                return React.DOM.div({className: 'loading'});

            return React.DOM.div({id: 'app-detail'},
                React.DOM.h1({}, app.get('name_or_id')),
                Cards.Rating({rating: app.get('rating')}),
                Cards.ApplicationCard({application: app, onLaunch: this.showModal}),
                React.DOM.h2({}, "Description"),
                React.DOM.p({}, app.get('description')),
                React.DOM.h2({}, "Versions of this Image"),
                MachineList({machines: app.get('machines')}),
                LaunchApplicationModal({application: app}));
        }
    });

    return ApplicationDetail;
});
<commit_msg>Refaceoted image launch modal to use modal events<commit_after>define(['react', 'models/application', 'collections/applications',
'components/images/cards', 'components/mixins/modal', 'jquery',
'components/common/time', 'modal', 'singletons/providers',
'singletons/profile'], function(React, App, AppCollection, Cards, ModalMixin,
$, Time, Modal, providers, profile) {

    var Machine = React.createClass({
        render: function() {
            var machine = this.props.machine;
            console.log(machine);
            return React.DOM.li({}, "Version: ", machine.get('pretty_version'),
                React.DOM.br(),
                "Date: ", Time({date: machine.get('start_date'), showRelative: false}));
        }
    });

    var MachineList = React.createClass({
        render: function() {
            var versions = this.props.machines.map(function(model) {
                return Machine({key: model.id, machine: model});
            });
            return React.DOM.ul({}, versions);
        }
    });

    var LaunchApplicationModal = React.createClass({
        mixins: [ModalMixin],
        getInitialState: function() {
            return {
                instanceName: '',
                identityId: '',
            };
        },
        renderTitle: function() {
            return this.props.application.get('name_or_id');
        },
        updateState: function(key, e) {
            var value = e.target.value;
            var state = {};
            state[key] = value;
            this.setState(state);
        },
        renderIdentityList: function() {
            var options = profile.get('identities').map(function(identity) {
                var provider_name = providers.get(identity.get('provider_id')).get('name');
                return React.DOM.option({value: identity.id},
                    "Identity " + identity.id + " on " + provider_name);
            });
            var callback = _.bind(this.updateState, this, 'identityId');
            return React.DOM.select({
                value: this.state.identityId,
                id: 'identity',
                className: 'form-control',
                onChange: callback}, options);
        },
        renderBody: function() {
            // provider id, identity id, machine_alias, name, size_alias, tags
            console.log(providers);
            console.log(profile);
            return React.DOM.form({role: 'form'},
                React.DOM.div({className: 'form-group'},
                    React.DOM.label({htmlFor: 'instance-name'}, "Instance Name"),
                    React.DOM.input({type: 'text', className: 'form-control', id: 'instance-name'})),
                React.DOM.div({className: 'form-group'},
                    React.DOM.label({htmlFor: 'identity'}, "Identity"),
                    this.renderIdentityList()));
        },
        renderFooter: function() {
            return React.DOM.button({type: 'submit', className: 'btn btn-default'}, "Launch");
        }
    });

    var ApplicationDetail = React.createClass({
        getInitialState: function() {
            return {application: null};
        },
        componentDidMount: function() {
            // TODO: This is what should happen if we have API support for it
            /*
            var app = new App({id: this.props.image_id});
            app.fetch({success: function(model) {
                this.setState({image: model});
            }.bind(this)});
            */
            // This is terrible.
            var apps = new AppCollection();
            apps.fetch({success: function(collection) {
                var app = collection.get(this.props.image_id);
                this.setState({application: app});
            }.bind(this)});
        },
        showModal: function(e) {
            Modal.events.trigger('alert', function(onClose) {
                return LaunchApplicationModal({
                    onClose: onClose,
                    application: this.state.application
                });
            }.bind(this));
            //$('#launch-modal').modal('show');
        },
        render: function() {
            var app = this.state.application;

            if (!app)
                return React.DOM.div({className: 'loading'});

            return React.DOM.div({id: 'app-detail'},
                React.DOM.h1({}, app.get('name_or_id')),
                Cards.Rating({rating: app.get('rating')}),
                Cards.ApplicationCard({application: app, onLaunch: this.showModal}),
                React.DOM.h2({}, "Description"),
                React.DOM.p({}, app.get('description')),
                React.DOM.h2({}, "Versions of this Image"),
                MachineList({machines: app.get('machines')}))
        }
    });

    return ApplicationDetail;
});
<|endoftext|>


<commit_before>ko.bindingHandlers.alert = {
    init: function () {
        return { controlsDescendantBindings: true };
    },

    update: function (element, valueAccessor) {
        var value = valueAccessor(),
            usedTemplateEngine = !value.name ? ko.stringTemplateEngine.instance : null,
            name = ko.unwrap(value.name) || 'alert',

            data = value.data || {
                message: value.message,
                priority: ko.computed(function() {
                    return 'alert-' + ko.unwrap(value.priority);
                }),

                close: value.close || function () {
                    ko.virtualElements.emptyNode(element);
                }
            };

        ko.renderTemplate(name, data, ko.utils.extend({ templateEngine: usedTemplateEngine }, value.templateOptions), element);
    }
};

ko.virtualElements.allowedBindings.alert = true;<commit_msg>Renamed 'name' property to 'template' in alert binding<commit_after>ko.bindingHandlers.alert = {
    init: function () {
        return { controlsDescendantBindings: true };
    },

    update: function (element, valueAccessor) {
        var value = valueAccessor(),
            usedTemplateEngine = !value.template ? ko.stringTemplateEngine.instance : null,
            template = ko.unwrap(value.template) || 'alert',

            data = value.data || {
                message: value.message,
                priority: ko.computed(function() {
                    return 'alert-' + ko.unwrap(value.priority);
                }),

                close: value.close || function () {
                    ko.virtualElements.emptyNode(element);
                }
            };

        ko.renderTemplate(template, data, ko.utils.extend({ templateEngine: usedTemplateEngine }, value.templateOptions), element);
    }
};

ko.virtualElements.allowedBindings.alert = true;<|endoftext|>


<commit_before>var fs = require('fs'),
    helpers = require('./helpers'),
    FormData = require('form-data');

class Request {

    constructor(requestKey, config, userArgs) {
        var defaults = {
            protocol: 'http:',
            method: 'GET',
            endpoint: '',
            query: '',
            headers: {},
            body: '',
            env: '127.0.0.1'
        };

        var sanitized = Object.assign(defaults, helpers.cloneWithDefinedProps(config), helpers.cloneWithDefinedProps(userArgs));

        this.url =  this._getUrl(sanitized.protocol, sanitized.env, sanitized.endpoint, sanitized.query)
        this.config = this._parseConfig(sanitized, requestKey);
    }

    _parseConfig(config, requestKey) {
        var c = {};
        c.method = config.method;
        c.body = this._getBody(requestKey, config.headers, config.body)
        c.headers = this._getHeaders(config, c.body)
        return c;
    }

    _getUrl(protocol, env, endpoint, query) {
        return `${protocol}//${env}/${endpoint ? endpoint : ''}${query ? '?' + query : ''}`
    }

    _getBody(requestKey, headers, body) {

        var contentType = helpers.getPropSafely(headers, 'Content-Type');

        if (contentType === 'application/json') {
            body = JSON.stringify(body);
        } else if (contentType === 'application/x-www-form-urlencoded') {
            body = qs.stringify(body);
        } else if (contentType === 'multipart/form-data') {
            body = Object.keys(body).reduce((form, prop) => {

                var append = helpers.isFilePath(body[prop]) ?
                    fs.createReadStream(helpers.getAbsoluteFilePath(body[prop])) :
                    body[prop];

                form.append(prop, append);

                return form;
            }, new FormData());
        } else if (typeof body === 'object') {
        //TODO create error constants
        console.log(chalk.bold.red(`Error in \"${requestKey}\" request: A Content-Type header is required if request body is an object:`));
        console.log(chalk.red(`${JSON.stringify(r, null, 4)}`));
        process.exit(1);
        }
        return body;
    }

    _getHeaders(config, body) {

        var contentHeaders;

        if (body !== '') {
            if (body instanceof FormData) {
                contentHeaders = body.getHeaders();
            } else {
                contentHeaders = { 'Content-Length': Buffer.byteLength(body) }
            }
        }

        return Object.assign(config.headers, contentHeaders || {});
    }
}

module.exports = Request;
<commit_msg>formatting<commit_after>var fs = require('fs'),
    helpers = require('./helpers'),
    FormData = require('form-data');

class Request {

    constructor(requestKey, config, userArgs) {

        var defaults = {
            protocol: 'http:',
            method: 'GET',
            endpoint: '',
            query: '',
            headers: {},
            body: '',
            env: '127.0.0.1'
        };

        var sanitized = Object.assign(defaults,
                                      helpers.cloneWithDefinedProps(config),
                                      helpers.cloneWithDefinedProps(userArgs));

        this.url =  this._getUrl(sanitized.protocol,
                                 sanitized.env,
                                 sanitized.endpoint,
                                 sanitized.query);

        this.config = this._parseConfig(sanitized, requestKey);
    }

    _parseConfig(config, requestKey) {
        var c = {};
        c.method = config.method;
        c.body = this._getBody(requestKey, config.headers, config.body);
        c.headers = this._getHeaders(config, c.body);
        return c;
    }

    _getUrl(protocol, env, endpoint, query) {
        return `${protocol}//${env}/${endpoint ? endpoint : ''}${query ? '?' + query : ''}`
    }

    _getBody(requestKey, headers, body) {

        var contentType = helpers.getPropSafely(headers, 'Content-Type');

        if (contentType === 'application/json') {
            body = JSON.stringify(body);
        } else if (contentType === 'application/x-www-form-urlencoded') {
            body = qs.stringify(body);
        } else if (contentType === 'multipart/form-data') {
            body = Object.keys(body).reduce((form, prop) => {

                var append = helpers.isFilePath(body[prop]) ?
                    fs.createReadStream(helpers.getAbsoluteFilePath(body[prop])) :
                    body[prop];

                form.append(prop, append);

                return form;
            }, new FormData());
        } else if (typeof body === 'object') {
        //TODO create error constants
        console.log(chalk.bold.red(`Error in \"${requestKey}\" request: A Content-Type header is required if request body is an object:`));
        console.log(chalk.red(`${JSON.stringify(r, null, 4)}`));
        process.exit(1);
        }

        return body;
    }

    _getHeaders(config, body) {

        var contentHeaders;

        if (body !== '') {
            if (body instanceof FormData) {
                contentHeaders = body.getHeaders();
            } else {
                contentHeaders = { 'Content-Length': Buffer.byteLength(body) };
            }
        }

        return Object.assign(config.headers, contentHeaders || {});
    }
}

module.exports = Request;
<|endoftext|>


<commit_before>iD.services.taginfo = function() {
    var taginfo = {},
        endpoint = 'https://taginfo.openstreetmap.org/api/4/',
        tag_sorts = {
            point: 'count_nodes',
            vertex: 'count_nodes',
            area: 'count_ways',
            line: 'count_ways'
        },
        tag_filters = {
            point: 'nodes',
            vertex: 'nodes',
            area: 'ways',
            line: 'ways'
        };

    function sets(parameters, n, o) {
        if (parameters.geometry && o[parameters.geometry]) {
            parameters[n] = o[parameters.geometry];
        }
        return parameters;
    }

    function setFilter(parameters) {
        return sets(parameters, 'filter', tag_filters);
    }

    function setSort(parameters) {
        return sets(parameters, 'sortname', tag_sorts);
    }

    function clean(parameters) {
        return _.omit(parameters, 'geometry', 'debounce');
    }

    function popularKeys(parameters) {
        var pop_field = 'count_all';
        if (parameters.filter) pop_field = 'count_' + parameters.filter;
        return function(d) { return parseFloat(d[pop_field]) > 5000 || d.in_wiki; };
    }

    function popularValues() {
        return function(d) { return parseFloat(d.fraction) > 0.01 || d.in_wiki; };
    }

    function valKey(d) {
        return {
            value: d.key,
            title: d.key
        };
    }

    function valKeyDescription(d) {
        return {
            value: d.value,
            title: d.description || d.value
        };
    }

    // sort keys with ':' lower than keys without ':'
    function sortKeys(a, b) {
        return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1
            : (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1
            : 0;
    }

    var debounced = _.debounce(d3.json, 100, true);

    function request(url, debounce, callback) {
        var cache = iD.services.taginfo.cache;

        if (cache[url]) {
            callback(null, cache[url]);
        } else if (debounce) {
            debounced(url, done);
        } else {
            d3.json(url, done);
        }

        function done(err, data) {
            if (!err) cache[url] = data;
            callback(err, data);
        }
    }

    taginfo.keys = function(parameters, callback) {
        var debounce = parameters.debounce;
        parameters = clean(setSort(parameters));
        request(endpoint + 'keys/all?' +
            iD.util.qsString(_.extend({
                rp: 10,
                sortname: 'count_all',
                sortorder: 'desc',
                page: 1
            }, parameters)), debounce, function(err, d) {
                if (err) return callback(err);
                callback(null, d.data.filter(popularKeys(parameters)).sort(sortKeys).map(valKey));
            });
    };

    taginfo.values = function(parameters, callback) {
        var debounce = parameters.debounce;
        parameters = clean(setSort(setFilter(parameters)));
        request(endpoint + 'key/values?' +
            iD.util.qsString(_.extend({
                rp: 25,
                sortname: 'count_all',
                sortorder: 'desc',
                page: 1
            }, parameters)), debounce, function(err, d) {
                if (err) return callback(err);
                callback(null, d.data.filter(popularValues()).map(valKeyDescription), parameters);
            });
    };

    taginfo.docs = function(parameters, callback) {
        var debounce = parameters.debounce;
        parameters = clean(setSort(parameters));

        var path = 'key/wiki_pages?';
        if (parameters.value) path = 'tag/wiki_pages?';
        else if (parameters.rtype) path = 'relation/wiki_pages?';

        request(endpoint + path + iD.util.qsString(parameters), debounce, function(err, d) {
            if (err) return callback(err);
            callback(null, d.data);
        });
    };

    taginfo.endpoint = function(_) {
        if (!arguments.length) return endpoint;
        endpoint = _;
        return taginfo;
    };

    taginfo.reset = function() {
        iD.services.taginfo.cache = {};
        return taginfo;
    };

    if (!iD.services.taginfo.cache) {
        taginfo.reset();
    }

    return taginfo;
};
<commit_msg>Lower popularity thresholds for taginfo lookups https://github.com/openstreetmap/iD/pull/3080#issuecomment-214881846<commit_after>iD.services.taginfo = function() {
    var taginfo = {},
        endpoint = 'https://taginfo.openstreetmap.org/api/4/',
        tag_sorts = {
            point: 'count_nodes',
            vertex: 'count_nodes',
            area: 'count_ways',
            line: 'count_ways'
        },
        tag_filters = {
            point: 'nodes',
            vertex: 'nodes',
            area: 'ways',
            line: 'ways'
        };

    function sets(parameters, n, o) {
        if (parameters.geometry && o[parameters.geometry]) {
            parameters[n] = o[parameters.geometry];
        }
        return parameters;
    }

    function setFilter(parameters) {
        return sets(parameters, 'filter', tag_filters);
    }

    function setSort(parameters) {
        return sets(parameters, 'sortname', tag_sorts);
    }

    function clean(parameters) {
        return _.omit(parameters, 'geometry', 'debounce');
    }

    function popularKeys(parameters) {
        var pop_field = 'count_all';
        if (parameters.filter) pop_field = 'count_' + parameters.filter;
        return function(d) { return parseFloat(d[pop_field]) > 2500 || d.in_wiki; };
    }

    function popularValues() {
        return function(d) { return parseFloat(d.fraction) > 0.0 || d.in_wiki; };
    }

    function valKey(d) {
        return {
            value: d.key,
            title: d.key
        };
    }

    function valKeyDescription(d) {
        return {
            value: d.value,
            title: d.description || d.value
        };
    }

    // sort keys with ':' lower than keys without ':'
    function sortKeys(a, b) {
        return (a.key.indexOf(':') === -1 && b.key.indexOf(':') !== -1) ? -1
            : (a.key.indexOf(':') !== -1 && b.key.indexOf(':') === -1) ? 1
            : 0;
    }

    var debounced = _.debounce(d3.json, 100, true);

    function request(url, debounce, callback) {
        var cache = iD.services.taginfo.cache;

        if (cache[url]) {
            callback(null, cache[url]);
        } else if (debounce) {
            debounced(url, done);
        } else {
            d3.json(url, done);
        }

        function done(err, data) {
            if (!err) cache[url] = data;
            callback(err, data);
        }
    }

    taginfo.keys = function(parameters, callback) {
        var debounce = parameters.debounce;
        parameters = clean(setSort(parameters));
        request(endpoint + 'keys/all?' +
            iD.util.qsString(_.extend({
                rp: 10,
                sortname: 'count_all',
                sortorder: 'desc',
                page: 1
            }, parameters)), debounce, function(err, d) {
                if (err) return callback(err);
                callback(null, d.data.filter(popularKeys(parameters)).sort(sortKeys).map(valKey));
            });
    };

    taginfo.values = function(parameters, callback) {
        var debounce = parameters.debounce;
        parameters = clean(setSort(setFilter(parameters)));
        request(endpoint + 'key/values?' +
            iD.util.qsString(_.extend({
                rp: 25,
                sortname: 'count_all',
                sortorder: 'desc',
                page: 1
            }, parameters)), debounce, function(err, d) {
                if (err) return callback(err);
                callback(null, d.data.filter(popularValues()).map(valKeyDescription), parameters);
            });
    };

    taginfo.docs = function(parameters, callback) {
        var debounce = parameters.debounce;
        parameters = clean(setSort(parameters));

        var path = 'key/wiki_pages?';
        if (parameters.value) path = 'tag/wiki_pages?';
        else if (parameters.rtype) path = 'relation/wiki_pages?';

        request(endpoint + path + iD.util.qsString(parameters), debounce, function(err, d) {
            if (err) return callback(err);
            callback(null, d.data);
        });
    };

    taginfo.endpoint = function(_) {
        if (!arguments.length) return endpoint;
        endpoint = _;
        return taginfo;
    };

    taginfo.reset = function() {
        iD.services.taginfo.cache = {};
        return taginfo;
    };

    if (!iD.services.taginfo.cache) {
        taginfo.reset();
    }

    return taginfo;
};
<|endoftext|>


<commit_before>function clearSidebar() {
    Cookies.set('sidebar', []);
}

function getSidebar() {
    if (!Array.isArray(Cookies.getJSON('sidebar'))) {
        clearSidebar();
    }
    return Cookies.getJSON('sidebar');
}

function isExpanded(checkbox) {
    var id = $(checkbox).attr('id');

    var sidebar = getSidebar();
    for (var i = 0; i < sidebar.length; i++) {
        if (sidebar[i].name == id && sidebar[i].value == true)
            return 'checked';
    }
    return '';
}

function setExpanded(checkbox) {
    var id = $(checkbox).attr('id');
    var checked = $(checkbox).is(':checked');
    // alert('id = ' + id + ", checked = " + checked );

    var found = false;
    var checkedArray = getSidebar();
    for (var i = 0; i < checkedArray.length; i++) {
        if (checkedArray[i].name == id) {
            checkedArray[i].value = checked;
            found = true;
            break;
        }
    }
    if (!found) {
        checkedArray.push({name: id, value: checked});
    }

    Cookies.set('sidebar', checkedArray);
}

// alert(Cookies.get('sidebar'));
<commit_msg>SITE-22: Use jquery for isArray().<commit_after>function clearSidebar() {
    Cookies.set('sidebar', []);
}

function getSidebar() {
    if (!$.isArray(Cookies.getJSON('sidebar'))) {
        clearSidebar();
    }
    return Cookies.getJSON('sidebar');
}

function isExpanded(checkbox) {
    var id = $(checkbox).attr('id');

    var sidebar = getSidebar();
    for (var i = 0; i < sidebar.length; i++) {
        if (sidebar[i].name == id && sidebar[i].value == true)
            return 'checked';
    }
    return '';
}

function setExpanded(checkbox) {
    var id = $(checkbox).attr('id');
    var checked = $(checkbox).is(':checked');
    // alert('id = ' + id + ", checked = " + checked );

    var found = false;
    var checkedArray = getSidebar();
    for (var i = 0; i < checkedArray.length; i++) {
        if (checkedArray[i].name == id) {
            checkedArray[i].value = checked;
            found = true;
            break;
        }
    }
    if (!found) {
        checkedArray.push({name: id, value: checked});
    }

    Cookies.set('sidebar', checkedArray);
}

// alert(Cookies.get('sidebar'));
<|endoftext|>


<commit_before>import Icon from '@conveyal/woonerf/components/icon'
import moment from 'moment'
import React, {Component, PropTypes} from 'react'
import {ListGroup, Table, ListGroupItem, Panel, Button, Col, Row} from 'react-bootstrap'
import {browserHistory} from 'react-router'

import {isEditingDisabled} from '../../util'
import {getTableFatalExceptions} from '../../util/version'
import Loading from '../../../common/components/Loading'
import OptionButton from '../../../common/components/OptionButton'
import toSentenceCase from '../../../common/util/to-sentence-case'

const DEFAULT_LIMIT = 10

export default class GtfsValidationViewer extends Component {
  static propTypes = {
    fetchValidationIssueCount: PropTypes.func,
    fetchGTFSEntities: PropTypes.func,
    validationResult: PropTypes.object,
    version: PropTypes.object
  }

  state = {
    offset: 0,
    limit: DEFAULT_LIMIT
  }

  componentWillMount () {
    this.props.fetchValidationIssueCount()
  }

  componentWillReceiveProps (nextProps) {
    if (
      this._validationHasErrors(this.props.validationResult) &&
      !this._validationHasErrors(nextProps.validationResult)
    ) {
      // If the validation error count was overwritten by fetching feed versions,
      // refetch the issue count.
      this.props.fetchValidationIssueCount()
    }
  }

  _onClickErrorType = errorType => {
    const {version: feedVersion, fetchValidationErrors} = this.props
    const {active, limit, offset} = this.state
    const {error_counts: errorCounts} = feedVersion.validationResult
    if (active === errorType) {
      // Close error type category
      this.setState({active: null})
    } else {
      const category = errorCounts.find(c => c.type === errorType)
      if (!category.errors || category.errors.length === 0) {
        // Fetch errors and reset active error type, limit, and offset
        this.setState({active: errorType})
        fetchValidationErrors({feedVersion, errorType, offset, limit})
      } else {
        // Do not fetch errors if first batch has already been fetched.
        // reset active error type, limit, and offset
        this.setState({active: errorType, offset: 0})
      }
    }
  }

  _validationHasErrors = (result) => {
    return result && result.error_counts
  }

  _onClickLoadMoreErrors = errorType => {
    const {version: feedVersion, fetchValidationErrors} = this.props
    const {limit} = this.state
    const {error_counts: errorCounts} = feedVersion.validationResult
    const category = errorCounts.find(c => c.type === errorType)
    // Update offset based on number of errors already fetched
    const offset = category.errors.length
    this.setState({offset})
    fetchValidationErrors({feedVersion, errorType, offset, limit})
  }

  render () {
    const {
      validationResult: result,
      version
    } = this.props
    const tableFatalExceptions = getTableFatalExceptions(version)
    const {active} = this.state
    const dateFormat = 'MMM. DD, YYYY'
    const timeFormat = 'h:MMa'
    const hasValidation = this._validationHasErrors(result)
    const versionHasErrors = hasValidation && result.error_counts.length

    let validationContent = null
    if (!hasValidation) { // Validation not retreived yet
      validationContent = (
        <ListGroup fill>
          <ListGroupItem className='validation-item'>
            Retrieving Validation...
          </ListGroupItem>
        </ListGroup>
      )
    } else if (versionHasErrors) {
      validationContent = result.error_counts.map((category, index) => {
        const errorTypeIsActive = active === category.type
        const firstError = category.errors && category.errors[0]
        let multiTyped = false
        firstError && category.errors.forEach(e => {
          if (e.entity_type !== firstError.entity_type) multiTyped = true
        })
        const tableType = category.type.indexOf('TABLE') !== -1
        const includeEntityType = tableType || multiTyped
        return (
          <ListGroup key={index} fill>
            <ListGroupItem>
              <Row
                className='list-group-item-heading'
                // FIXME
                onClick={() => this._onClickErrorType(category.type)}
                style={{cursor: 'pointer'}}>
                <Col xs={12}>

                    <Icon type={errorTypeIsActive ? 'caret-down' : 'caret-right'} />
                    {toSentenceCase(category.type.replace(/_/g, ' '))}

                      <Icon type='exclamation-triangle' /> {category.count}

                </Col>
              </Row>
            </ListGroupItem>
            {/* Render list of errors */}
            <Panel collapsible bsClass='' expanded={errorTypeIsActive}>
              <ListGroupItem>
                Description: {category.message}
              </ListGroupItem>

                  Line #
                  {includeEntityType && Entity type	}
                  {firstError && !includeEntityType ? firstError.entity_type : 'Entity'} ID
                  Bad value
                  Action

                {category.errors
                  ? category.errors.map((error, index) => (
                    <ValidationErrorItem
                      {...this.props}
                      version={version}
                      multipleEntityTypes={includeEntityType}
                      error={error}
                      index={index}
                      key={index} />
                  ))
                  : <Loading />
                }

            </Panel>
            {errorTypeIsActive &&
              category.errors &&
              category.errors.length < category.count
              ? <ListGroupItem className='validation-item'>
                <OptionButton
                  value={category.type}
                  onClick={this._onClickLoadMoreErrors}>
                  Load more
                </OptionButton>
              </ListGroupItem>
              : errorTypeIsActive
                ? <ListGroupItem className='validation-item'>
                  No more errors of this type
                </ListGroupItem>
                : null
            }
          </ListGroup>
        )
      })
    } else { // No errors found
      validationContent = (
        <ListGroup fill>
          <ListGroupItem className='validation-item'>
            No validation errors!
          </ListGroupItem>
        </ListGroup>
      )
    }

    return (

          {version.name}{' '}

            {moment(version.updated).format(dateFormat + ', ' + timeFormat)}

        {tableFatalExceptions.length > 0 &&

Fatal exceptions: {tableFatalExceptions.map(e => `${e.tableName}: ${e.fatalException}`).join(', ')}

        }
        <Panel header={
Validation errors
}>
          {validationContent}
        </Panel>

    )
  }
}

class ValidationErrorItem extends Component {
  state = {expanded: false}

  _onClickFetch = () => {
    const {expanded} = this.state
    const {error, gtfs, version: feedVersion, fetchGTFSEntities} = this.props
    const {entity_id: id, entity_type: type} = error
    const entity = gtfs.validation.data.find(e => e._id === `${type}:${id}`)
    if (!expanded) {
      if (!entity) {
        // fetch GTFS entity
        const {entity_id: id, entity_type: type} = error
        if (id) {
          fetchGTFSEntities({type, id: [id], namespace: feedVersion.namespace})
        } else {
          // FIXME: fetch by line number
          fetchGTFSEntities({type, id: [id], namespace: feedVersion.namespace})
        }
      }
    }
    this.setState({expanded: !expanded})
  }

  _renderEntityDetails = (entity, type, sequence) => {
    if (entity) {
      if (type === 'StopTime') {
        const items = [
...
]
        items.push(entity.stop_times.map((stopTime, index) => {
          if (stopTime.stop_sequence < sequence - 1 || stopTime.stop_sequence > sequence + 1) return null
          return

            {Object.keys(stopTime).map(stKey => {
              let value = stopTime[stKey]
              if (stKey.endsWith('_time')) {
                const time = moment().startOf('day').seconds(stopTime[stKey])
                value = time && time.isValid() ? time.format('HH:mm:ss') : value
              }
              return
{stKey}: {value}

            })}

        }))
        items.push(
...
)
        return items
      }
      return (

          {Object.keys(entity).map(key => {
            if (key === '_id') return null
            return
{key}: {entity[key]}

          })}

      )
    } else {
      return null
    }
  }

  // FIXME: Add link to GTFS Editor with entity_id
  _onEditClick = () => {
    const {error, version} = this.props
    // Line numbers in validation errors also refer to unique ID field of each
    // table of entities
    const {line_number: id, entity_type: type} = error
    const component = type && type.toLowerCase()
    if (component) {
      browserHistory.push(`/feed/${version.feedSource.id}/edit/${component}/${id}`)
    } else {
      console.warn(`Error does not have type. Cannot link to editor.`, error)
    }
  }

  render () {
    const {error, feedSource, gtfs, index, multipleEntityTypes, project, user} = this.props
    const {expanded} = this.state
    const {
      entity_id: gtfsId,
      entity_sequence: sequence,
      entity_type: type,
      line_number: lineNumber,
      bad_value: badValue
    } = error
    const entity = gtfs.validation.data.find(e => e._id === `${type}:${gtfsId}`)
    const noFeedInEditor = feedSource.editorNamespace === null
    const editableTypes = ['stop', 'route']
    const idString = sequence !== null
      ? `${gtfsId}:${sequence}`
      : gtfsId
    const component = type && type.toLowerCase()
    const typeIsNotEditable = typeof type === 'undefined' || editableTypes.indexOf(component) === -1
    const editingIsDisabled = isEditingDisabled(user, feedSource, project) ||
      typeIsNotEditable ||
      noFeedInEditor
    return

      {index % 2 === 0 ?
 : null}

        {lineNumber}
        {multipleEntityTypes && {type}	}
        {idString}
        {badValue === null ? N/A : badValue}

          <Button
            disabled={editingIsDisabled}
            style={{margin: 0, padding: 0}}
            title={noFeedInEditor
              ? 'Import feed into editor to fix issue'
              : typeIsNotEditable
                ? 'Error type is not linked to editor'
                : 'Fix in editor'}
            onClick={this._onEditClick}
            bsStyle='link'
          >
            <Icon type='wrench' />
          </Button>
          <Button
            onClick={this._onClickFetch}
            title={`View ${type} details`}
            style={{margin: 0, padding: 0}}
            bsStyle='link'
          >
            <Icon type='info' />
          </Button>

      {expanded ? this._renderEntityDetails(entity, type, sequence) : null}

  }
}
<commit_msg>fix(manager): restrict view entity details to issues with entity IDs<commit_after>import Icon from '@conveyal/woonerf/components/icon'
import moment from 'moment'
import React, {Component, PropTypes} from 'react'
import {ListGroup, Table, ListGroupItem, Panel, Button, Col, Row} from 'react-bootstrap'
import {browserHistory} from 'react-router'

import {isEditingDisabled} from '../../util'
import {getTableFatalExceptions} from '../../util/version'
import Loading from '../../../common/components/Loading'
import OptionButton from '../../../common/components/OptionButton'
import toSentenceCase from '../../../common/util/to-sentence-case'

const DEFAULT_LIMIT = 10

export default class GtfsValidationViewer extends Component {
  static propTypes = {
    fetchValidationIssueCount: PropTypes.func,
    fetchGTFSEntities: PropTypes.func,
    validationResult: PropTypes.object,
    version: PropTypes.object
  }

  state = {
    offset: 0,
    limit: DEFAULT_LIMIT
  }

  componentWillMount () {
    this.props.fetchValidationIssueCount()
  }

  componentWillReceiveProps (nextProps) {
    if (
      this._validationHasErrors(this.props.validationResult) &&
      !this._validationHasErrors(nextProps.validationResult)
    ) {
      // If the validation error count was overwritten by fetching feed versions,
      // refetch the issue count.
      this.props.fetchValidationIssueCount()
    }
  }

  _onClickErrorType = errorType => {
    const {version: feedVersion, fetchValidationErrors} = this.props
    const {active, limit, offset} = this.state
    const {error_counts: errorCounts} = feedVersion.validationResult
    if (active === errorType) {
      // Close error type category
      this.setState({active: null})
    } else {
      const category = errorCounts.find(c => c.type === errorType)
      if (!category.errors || category.errors.length === 0) {
        // Fetch errors and reset active error type, limit, and offset
        this.setState({active: errorType})
        fetchValidationErrors({feedVersion, errorType, offset, limit})
      } else {
        // Do not fetch errors if first batch has already been fetched.
        // reset active error type, limit, and offset
        this.setState({active: errorType, offset: 0})
      }
    }
  }

  _validationHasErrors = (result) => {
    return result && result.error_counts
  }

  _onClickLoadMoreErrors = errorType => {
    const {version: feedVersion, fetchValidationErrors} = this.props
    const {limit} = this.state
    const {error_counts: errorCounts} = feedVersion.validationResult
    const category = errorCounts.find(c => c.type === errorType)
    // Update offset based on number of errors already fetched
    const offset = category.errors.length
    this.setState({offset})
    fetchValidationErrors({feedVersion, errorType, offset, limit})
  }

  render () {
    const {
      validationResult: result,
      version
    } = this.props
    const tableFatalExceptions = getTableFatalExceptions(version)
    const {active} = this.state
    const dateFormat = 'MMM. DD, YYYY'
    const timeFormat = 'h:MMa'
    const hasValidation = this._validationHasErrors(result)
    const versionHasErrors = hasValidation && result.error_counts.length

    let validationContent = null
    if (!hasValidation) { // Validation not retreived yet
      validationContent = (
        <ListGroup fill>
          <ListGroupItem className='validation-item'>
            Retrieving Validation...
          </ListGroupItem>
        </ListGroup>
      )
    } else if (versionHasErrors) {
      validationContent = result.error_counts.map((category, index) => {
        const errorTypeIsActive = active === category.type
        const firstError = category.errors && category.errors[0]
        let multiTyped = false
        firstError && category.errors.forEach(e => {
          if (e.entity_type !== firstError.entity_type) multiTyped = true
        })
        const tableType = category.type.indexOf('TABLE') !== -1
        const includeEntityType = tableType || multiTyped
        return (
          <ListGroup key={index} fill>
            <ListGroupItem>
              <Row
                className='list-group-item-heading'
                // FIXME
                onClick={() => this._onClickErrorType(category.type)}
                style={{cursor: 'pointer'}}>
                <Col xs={12}>

                    <Icon type={errorTypeIsActive ? 'caret-down' : 'caret-right'} />
                    {toSentenceCase(category.type.replace(/_/g, ' '))}

                      <Icon type='exclamation-triangle' /> {category.count}

                </Col>
              </Row>
            </ListGroupItem>
            {/* Render list of errors */}
            <Panel collapsible bsClass='' expanded={errorTypeIsActive}>
              <ListGroupItem>
                Description: {category.message}
              </ListGroupItem>

                  Line #
                  {includeEntityType && Entity type	}
                  {firstError && !includeEntityType ? firstError.entity_type : 'Entity'} ID
                  Bad value
                  Action

                {category.errors
                  ? category.errors.map((error, index) => (
                    <ValidationErrorItem
                      {...this.props}
                      version={version}
                      multipleEntityTypes={includeEntityType}
                      error={error}
                      index={index}
                      key={index} />
                  ))
                  : <Loading />
                }

            </Panel>
            {errorTypeIsActive &&
              category.errors &&
              category.errors.length < category.count
              ? <ListGroupItem className='validation-item'>
                <OptionButton
                  value={category.type}
                  onClick={this._onClickLoadMoreErrors}>
                  Load more
                </OptionButton>
              </ListGroupItem>
              : errorTypeIsActive
                ? <ListGroupItem className='validation-item'>
                  No more errors of this type
                </ListGroupItem>
                : null
            }
          </ListGroup>
        )
      })
    } else { // No errors found
      validationContent = (
        <ListGroup fill>
          <ListGroupItem className='validation-item'>
            No validation errors!
          </ListGroupItem>
        </ListGroup>
      )
    }

    return (

          {version.name}{' '}

            {moment(version.updated).format(dateFormat + ', ' + timeFormat)}

        {tableFatalExceptions.length > 0 &&

Fatal exceptions: {tableFatalExceptions.map(e => `${e.tableName}: ${e.fatalException}`).join(', ')}

        }
        <Panel header={
Validation errors
}>
          {validationContent}
        </Panel>

    )
  }
}

class ValidationErrorItem extends Component {
  state = {expanded: false}

  _onClickFetch = () => {
    const {expanded} = this.state
    const {error, gtfs, version: feedVersion, fetchGTFSEntities} = this.props
    const {entity_id: id, entity_type: type} = error
    const entity = gtfs.validation.data.find(e => e._id === `${type}:${id}`)
    if (!expanded) {
      if (!entity) {
        // fetch GTFS entity
        const {entity_id: id, entity_type: type} = error
        if (id) {
          fetchGTFSEntities({type, id: [id], namespace: feedVersion.namespace})
        } else {
          // FIXME: fetch by line number
          fetchGTFSEntities({type, id: [id], namespace: feedVersion.namespace})
        }
      }
    }
    this.setState({expanded: !expanded})
  }

  _renderEntityDetails = (entity, type, sequence) => {
    if (entity) {
      if (type === 'StopTime') {
        const items = [
...
]
        items.push(entity.stop_times.map((stopTime, index) => {
          if (stopTime.stop_sequence < sequence - 1 || stopTime.stop_sequence > sequence + 1) return null
          return

            {Object.keys(stopTime).map(stKey => {
              let value = stopTime[stKey]
              if (stKey.endsWith('_time')) {
                const time = moment().startOf('day').seconds(stopTime[stKey])
                value = time && time.isValid() ? time.format('HH:mm:ss') : value
              }
              return
{stKey}: {value}

            })}

        }))
        items.push(
...
)
        return items
      }
      return (

          {Object.keys(entity).map(key => {
            if (key === '_id') return null
            return
{key}: {entity[key]}

          })}

      )
    } else {
      return null
    }
  }

  // FIXME: Add link to GTFS Editor with entity_id
  _onEditClick = () => {
    const {error, version} = this.props
    // Line numbers in validation errors also refer to unique ID field of each
    // table of entities
    const {line_number: id, entity_type: type} = error
    const component = type && type.toLowerCase()
    if (component) {
      browserHistory.push(`/feed/${version.feedSource.id}/edit/${component}/${id}`)
    } else {
      console.warn(`Error does not have type. Cannot link to editor.`, error)
    }
  }

  render () {
    const {error, feedSource, gtfs, index, multipleEntityTypes, project, user} = this.props
    const {expanded} = this.state
    const {
      entity_id: gtfsId,
      entity_sequence: sequence,
      entity_type: type,
      line_number: lineNumber,
      bad_value: badValue
    } = error
    const entity = gtfs.validation.data.find(e => e._id === `${type}:${gtfsId}`)
    const noFeedInEditor = feedSource.editorNamespace === null
    const editableTypes = ['stop', 'route']
    const noEntityId = !gtfsId
    const idString = sequence !== null
      ? `${gtfsId}:${sequence}`
      : gtfsId
    const component = type && type.toLowerCase()
    const typeIsNotEditable = typeof type === 'undefined' ||
      editableTypes.indexOf(component) === -1 ||
      noEntityId
    const editingIsDisabled = isEditingDisabled(user, feedSource, project) ||
      typeIsNotEditable ||
      noFeedInEditor
    return

      {index % 2 === 0 ?
 : null}

        {lineNumber}
        {multipleEntityTypes && {type}	}
        {idString}
        {badValue === null ? N/A : badValue}

          <Button
            disabled={editingIsDisabled}
            style={{margin: 0, padding: 0}}
            title={noFeedInEditor
              ? 'Import feed into editor to fix issue'
              : typeIsNotEditable
                ? 'Error type is not linked to editor'
                : 'Fix in editor'}
            onClick={this._onEditClick}
            bsStyle='link'
          >
            <Icon type='wrench' />
          </Button>
          <Button
            onClick={this._onClickFetch}
            disabled={noEntityId}
            title={noEntityId ? `There is no entity to view` : `View ${type} details`}
            style={{margin: 0, padding: 0}}
            bsStyle='link'
          >
            <Icon type='info' />
          </Button>

      {expanded ? this._renderEntityDetails(entity, type, sequence) : null}

  }
}
<|endoftext|>