<commit_before>// @flow

import {camelCaseKeys} from '../../../common/util/map-keys'
import clone from 'lodash/cloneDeep'
import objectPath from 'object-path'
import React, {Component} from 'react'
import {Button} from 'react-bootstrap'

import * as activeActions from '../../actions/active'
import * as tripActions from '../../actions/trip'
import {ENTITY} from '../../constants'
import {generateNullProps} from '../../util/gtfs'
import {entityIsNew} from '../../util/objects'
import {isTimeFormat} from '../../util/timetable'
import Timetable from './Timetable'
import TimetableHeader from './TimetableHeader'
import TimetableHelpModal from './TimetableHelpModal'

import type {Props as ContainerProps} from '../../containers/ActiveTimetableEditor'
import type {Feed, FetchStatus, Pattern, StopTime, TimetableColumn, Trip, TripCounts} from '../../../types'
import type {TripValidationIssues} from '../../selectors/timetable'
import type {EditorTables, TimetableState} from '../../../types/reducers'

type Props = ContainerProps & {
  activeCell?: ?string,
  activePattern: Pattern,
  activePatternId: number,
  activeSchedule: string,
  activeScheduleId: string,
  addNewTrip: typeof tripActions.addNewTrip,
  columns: Array<TimetableColumn>,
  data?: Array<Trip>,
  deleteTripsForCalendar: typeof tripActions.deleteTripsForCalendar,
  feedSource: Feed,
  fetchCalendarTripCountsForPattern: typeof tripActions.fetchCalendarTripCountsForPattern,
  fetchTripsForCalendar: typeof tripActions.fetchTripsForCalendar,
  offsetRows: typeof tripActions.offsetRows,
  removeTrips: typeof tripActions.removeTrips,
  saveTripsForCalendar: typeof tripActions.saveTripsForCalendar,
  setActiveCell: typeof tripActions.setActiveCell,
  setActiveEntity: typeof activeActions.setActiveEntity,
  setOffset: typeof tripActions.setOffset,
  setScrollIndexes: typeof tripActions.setScrollIndexes,
  tableData: EditorTables,
  timetable: TimetableState,
  timetableStatus: FetchStatus,
  toggleAllRows: typeof tripActions.toggleAllRows,
  toggleDepartureTimes: typeof tripActions.toggleDepartureTimes,
  toggleRowSelection: typeof tripActions.toggleRowSelection,
  tripCounts: TripCounts,
  tripValidationErrors: TripValidationIssues,
  updateCellValue: typeof tripActions.updateCellValue
}

type State = {
  height: number,
  showHelpModal: boolean,
  width: number
}

export default class TimetableEditor extends Component<Props, State> {
  // State is used to track height/width of the window to dynamically adjust
  // height of Grid as well as the visibility of the help modal.
  state = {
    height: 680,
    showHelpModal: false,
    width: 1280
  }

  _onResize = () => this.setState({width: window.innerWidth, height: window.innerHeight})

  _createNewCalendar = () => this.props.setActiveEntity(
    this.props.feedSource.id,
    'calendar',
    {id: ENTITY.NEW_ID}
  )

  _hideHelpModal = () => this.setState({showHelpModal: false})

  _showHelpModal = () => this.setState({showHelpModal: true})

  /**
   * Handle re-fetching trips if the schedule ID changes (or on initial mount).
   */
  componentWillReceiveProps (nextProps: Props) {
    const {activeScheduleId: previousScheduleId} = this.props
    const {
      feedSource,
      fetchTripsForCalendar,
      activePattern,
      activeSchedule,
      activeScheduleId,
      timetableStatus
    } = nextProps
    const scheduleIdChanged = previousScheduleId !== activeScheduleId
    if (feedSource && activePattern && activeSchedule) {
      // Only fetch trips if the feed, pattern, and schedule are present.
      if (scheduleIdChanged) {
        // Re-fetch on schedule ID change.
        fetchTripsForCalendar(feedSource.id, activePattern, activeScheduleId)
      } else if (!timetableStatus.fetched && !timetableStatus.fetching) {
        // Handle initial fetch
        fetchTripsForCalendar(feedSource.id, activePattern, activeScheduleId)
      }
    }
  }

  componentWillMount () {
    this._onResize()
  }

  componentDidMount () {
    window.addEventListener('resize', this._onResize)
  }

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

  /**
   * Returns an array of the currently selected indexes OR a singleton array
   * containing the row index where the cursor is presently active.
   */
  _getSelectedRowIndexes = () => {
    const {selected, scrollIndexes} = this.props.timetable
    return selected.length > 0
      ? selected
      : [scrollIndexes.scrollToRow]
  }

  _offsetWithDefaults = (negative: boolean = false) => {
    const {columns, offsetRows, timetable} = this.props
    const rowIndexes = this._getSelectedRowIndexes()
    const {offset: currentOffset} = timetable
    const offset = negative ? currentOffset * -1 : currentOffset
    if (offset === 0 || isNaN(offset)) {
      console.warn(`invalid offset value=${offset}`)
      return
    }
    offsetRows({columns, rowIndexes, offset})
  }

  cloneSelectedTrips = () => this.duplicateRows(this._getSelectedRowIndexes())

  constructNewRow = (toClone: ?Trip = null) => {
    const {activePatternId, route} = this.props
    const activePattern = route && route.tripPatterns
      ? route.tripPatterns.find(p => p.id === activePatternId)
      : null
    if (!activePattern) {
      console.warn('Pattern ID not found in route', route, activePatternId)
      return
    }
    // If cloning a row, use toClone object as a baseline object.
    const blankTrip: Trip = generateNullProps('trip')
    const newRow: Trip = toClone
      ? clone(toClone) || blankTrip
      : blankTrip
    if (toClone) {
      // Update trip's internal ID and trip ID if cloning.
      objectPath.set(newRow, 'id', ENTITY.NEW_ID)
      objectPath.set(newRow, 'tripId', null)
      return newRow
    } else {
      // set starting time for first arrival
      let cumulativeTravelTime = !toClone
        ? 0
        : objectPath.get(newRow, `stopTimes.0.arrivalTime`)

      // TODO: auto-add offset to any new trip?  No, for now. Perhaps we could
      // add toggle/checkbox that allows for this.
      // cumulativeTravelTime += this.props.timetable.offset

      for (let i = 0; i < activePattern.patternStops.length; i++) {
        const stop = activePattern.patternStops[i]
        // if stopTime null/undefined, set as new object
        if (!objectPath.get(newRow, `stopTimes.${i}`)) {
          // FIXME: Change all stop time props to be snake case. They are camel
          // case because that's how the previous GTFS Editor model object fields
          // were named. And unlike the other standard tables, trips and stop times
          // were handled in the client as camelcase.
          const blankStopTime: StopTime = camelCaseKeys(generateNullProps('stoptime'))
          objectPath.set(newRow, `stopTimes.${i}`, blankStopTime)
        }
        objectPath.set(newRow, `stopTimes.${i}.stopId`, stop.stopId)
        cumulativeTravelTime += +stop.defaultTravelTime

        // only set time if timepoint set to true or null
        // if (stop.timepoint === null || stop.timepoint) {
        objectPath.set(newRow, `stopTimes.${i}.arrivalTime`, cumulativeTravelTime)
        // }
        cumulativeTravelTime += +stop.defaultDwellTime
        // if (stop.timepoint === null || stop.timepoint) {
        objectPath.set(newRow, `stopTimes.${i}.departureTime`, cumulativeTravelTime)
        objectPath.set(newRow, `stopTimes.${i}.timepoint`, stop.timepoint || 0)
        objectPath.set(newRow, `stopTimes.${i}.shapeDistTraveled`, stop.shapeDistTraveled)
        // Use pattern stop index to set stop sequence. Stop sequences should all
        // be zero-based and incrementing in the editor, but in the case that
        // they're not (e.g., due to a bad import) simply default to the index.
        objectPath.set(newRow, `stopTimes.${i}.stopSequence`, i)
        // }
      }<commit_msg>fix(TimetableEditor): Add continuousPickup and Dropoff values to StopTimes object<commit_after>// @flow

import clone from 'lodash/cloneDeep'
import objectPath from 'object-path'
import React, {Component} from 'react'
import {Button} from 'react-bootstrap'

import {camelCaseKeys} from '../../../common/util/map-keys'
import * as activeActions from '../../actions/active'
import * as tripActions from '../../actions/trip'
import {ENTITY} from '../../constants'
import {generateNullProps} from '../../util/gtfs'
import {entityIsNew} from '../../util/objects'
import {isTimeFormat} from '../../util/timetable'
import type {Props as ContainerProps} from '../../containers/ActiveTimetableEditor'
import type {Feed, FetchStatus, Pattern, StopTime, TimetableColumn, Trip, TripCounts} from '../../../types'
import type {TripValidationIssues} from '../../selectors/timetable'
import type {EditorTables, TimetableState} from '../../../types/reducers'

import TimetableHelpModal from './TimetableHelpModal'
import TimetableHeader from './TimetableHeader'
import Timetable from './Timetable'

type Props = ContainerProps & {
  activeCell?: ?string,
  activePattern: Pattern,
  activePatternId: number,
  activeSchedule: string,
  activeScheduleId: string,
  addNewTrip: typeof tripActions.addNewTrip,
  columns: Array<TimetableColumn>,
  data?: Array<Trip>,
  deleteTripsForCalendar: typeof tripActions.deleteTripsForCalendar,
  feedSource: Feed,
  fetchCalendarTripCountsForPattern: typeof tripActions.fetchCalendarTripCountsForPattern,
  fetchTripsForCalendar: typeof tripActions.fetchTripsForCalendar,
  offsetRows: typeof tripActions.offsetRows,
  removeTrips: typeof tripActions.removeTrips,
  saveTripsForCalendar: typeof tripActions.saveTripsForCalendar,
  setActiveCell: typeof tripActions.setActiveCell,
  setActiveEntity: typeof activeActions.setActiveEntity,
  setOffset: typeof tripActions.setOffset,
  setScrollIndexes: typeof tripActions.setScrollIndexes,
  tableData: EditorTables,
  timetable: TimetableState,
  timetableStatus: FetchStatus,
  toggleAllRows: typeof tripActions.toggleAllRows,
  toggleDepartureTimes: typeof tripActions.toggleDepartureTimes,
  toggleRowSelection: typeof tripActions.toggleRowSelection,
  tripCounts: TripCounts,
  tripValidationErrors: TripValidationIssues,
  updateCellValue: typeof tripActions.updateCellValue
}

type State = {
  height: number,
  showHelpModal: boolean,
  width: number
}

export default class TimetableEditor extends Component<Props, State> {
  // State is used to track height/width of the window to dynamically adjust
  // height of Grid as well as the visibility of the help modal.
  state = {
    height: 680,
    showHelpModal: false,
    width: 1280
  }

  _onResize = () => this.setState({width: window.innerWidth, height: window.innerHeight})

  _createNewCalendar = () => this.props.setActiveEntity(
    this.props.feedSource.id,
    'calendar',
    {id: ENTITY.NEW_ID}
  )

  _hideHelpModal = () => this.setState({showHelpModal: false})

  _showHelpModal = () => this.setState({showHelpModal: true})

  /**
   * Handle re-fetching trips if the schedule ID changes (or on initial mount).
   */
  componentWillReceiveProps (nextProps: Props) {
    const {activeScheduleId: previousScheduleId} = this.props
    const {
      feedSource,
      fetchTripsForCalendar,
      activePattern,
      activeSchedule,
      activeScheduleId,
      timetableStatus
    } = nextProps
    const scheduleIdChanged = previousScheduleId !== activeScheduleId
    if (feedSource && activePattern && activeSchedule) {
      // Only fetch trips if the feed, pattern, and schedule are present.
      if (scheduleIdChanged) {
        // Re-fetch on schedule ID change.
        fetchTripsForCalendar(feedSource.id, activePattern, activeScheduleId)
      } else if (!timetableStatus.fetched && !timetableStatus.fetching) {
        // Handle initial fetch
        fetchTripsForCalendar(feedSource.id, activePattern, activeScheduleId)
      }
    }
  }

  componentWillMount () {
    this._onResize()
  }

  componentDidMount () {
    window.addEventListener('resize', this._onResize)
  }

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

  /**
   * Returns an array of the currently selected indexes OR a singleton array
   * containing the row index where the cursor is presently active.
   */
  _getSelectedRowIndexes = () => {
    const {selected, scrollIndexes} = this.props.timetable
    return selected.length > 0
      ? selected
      : [scrollIndexes.scrollToRow]
  }

  _offsetWithDefaults = (negative: boolean = false) => {
    const {columns, offsetRows, timetable} = this.props
    const rowIndexes = this._getSelectedRowIndexes()
    const {offset: currentOffset} = timetable
    const offset = negative ? currentOffset * -1 : currentOffset
    if (offset === 0 || isNaN(offset)) {
      console.warn(`invalid offset value=${offset}`)
      return
    }
    offsetRows({columns, rowIndexes, offset})
  }

  cloneSelectedTrips = () => this.duplicateRows(this._getSelectedRowIndexes())

  constructNewRow = (toClone: ?Trip = null) => {
    const {activePatternId, route} = this.props
    const activePattern = route && route.tripPatterns
      ? route.tripPatterns.find(p => p.id === activePatternId)
      : null
    if (!activePattern) {
      console.warn('Pattern ID not found in route', route, activePatternId)
      return
    }
    // If cloning a row, use toClone object as a baseline object.
    const blankTrip: Trip = generateNullProps('trip')
    const newRow: Trip = toClone
      ? clone(toClone) || blankTrip
      : blankTrip
    if (toClone) {
      // Update trip's internal ID and trip ID if cloning.
      objectPath.set(newRow, 'id', ENTITY.NEW_ID)
      objectPath.set(newRow, 'tripId', null)
      return newRow
    } else {
      // set starting time for first arrival
      let cumulativeTravelTime = !toClone
        ? 0
        : objectPath.get(newRow, `stopTimes.0.arrivalTime`)

      // TODO: auto-add offset to any new trip?  No, for now. Perhaps we could
      // add toggle/checkbox that allows for this.
      // cumulativeTravelTime += this.props.timetable.offset

      for (let i = 0; i < activePattern.patternStops.length; i++) {
        const stop = activePattern.patternStops[i]
        // if stopTime null/undefined, set as new object
        if (!objectPath.get(newRow, `stopTimes.${i}`)) {
          // FIXME: Change all stop time props to be snake case. They are camel
          // case because that's how the previous GTFS Editor model object fields
          // were named. And unlike the other standard tables, trips and stop times
          // were handled in the client as camelcase.
          const blankStopTime: StopTime = camelCaseKeys(generateNullProps('stoptime'))
          objectPath.set(newRow, `stopTimes.${i}`, blankStopTime)
        }
        objectPath.set(newRow, `stopTimes.${i}.stopId`, stop.stopId)
        cumulativeTravelTime += +stop.defaultTravelTime

        // only set time if timepoint set to true or null
        // if (stop.timepoint === null || stop.timepoint) {
        objectPath.set(newRow, `stopTimes.${i}.arrivalTime`, cumulativeTravelTime)
        // }
        cumulativeTravelTime += +stop.defaultDwellTime
        // if (stop.timepoint === null || stop.timepoint) {
        objectPath.set(newRow, `stopTimes.${i}.departureTime`, cumulativeTravelTime)
        objectPath.set(newRow, `stopTimes.${i}.continuousPickup`, stop.continuousPickup)
        objectPath.set(newRow, `stopTimes.${i}.continuousDropOff`, stop.continuousDropOff)
        objectPath.set(newRow, `stopTimes.${i}.timepoint`, stop.timepoint || 0)
        objectPath.set(newRow, `stopTimes.${i}.shapeDistTraveled`, stop.shapeDistTraveled)
        // Use pattern stop index to set stop sequence. Stop sequences should all
        // be zero-based and incrementing in the editor, but in the case that
        // they're not (e.g., due to a bad import) simply default to the index.
        objectPath.set(newRow, `stopTimes.${i}.stopSequence`, i)
        // }
      }
{'fraction_non_alphanumeric': 0.10791682324940499, 'fraction_numerical': 0.0017537266691719905, 'mean_word_length': 3.451352104822972, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 29, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6265475', 'n_tokens_mistral': 4660, 'n_tokens_neox': 4520, 'n_words': 1250}