<commit_before>/**
 * Imports
 */

import frameReducer, {
  createIteratorQ, checkBounds, isEqualSequence, updateAnimal,
  createIterator, getIterator, setAnimalPos, turn, getLastFrame
} from 'utils/frameReducer'
import createApi, {teacherBot, createDocs} from 'animalApis'
import ReadingChallenge from 'components/ReadingChallenge'
import WritingChallenge from 'components/WritingChallenge'
import {getInterval, resetAnimalPos} from 'utils/animal'
import initialGameState from 'utils/initialGameState'
import {component, element} from 'vdux'
import mapValues from '@f/map-values'
import stackTrace from 'stack-trace'
import setProp from '@f/set-prop'
import sleep from '@f/sleep'
import srand from '@f/srand'

/**
 * <Game/>
 */

function addTeacherBot (levelSize) {
  return {
    type: 'teacherBot',
    current: {
      location: [levelSize[0] - 1, 0],
      rot: 0
    },
    initial: {
      location: [levelSize[0] - 1, 0],
      rot: 0
    },
    hidden: false
  }
}

function toggleTeacherBot (animals, toggle) {
  return animals.map(a => {
    return a.type !== 'teacherBot'
			? {...a, hidden: toggle}
			: {...a, hidden: !toggle}
  })
}

export default component({
	initialState: ({props}) => ({
		ready: false,
    docs: createDocs(props.game.capabilities, props.game.palette)
	}),

  * onCreate ({state, actions, context, props}) {
		const {game, savedGame = {}, initialData = {}} = props
		if (Object.keys(initialData).length > 0) {
			const draftAnimals = initialData.advanced && initialData.solution
				? initialData.solution.concat(addTeacherBot(game.levelSize))
				: initialData.animals.map(a => resetAnimalPos(a, game.levelSize[0])).concat(addTeacherBot(game.levelSize))
			return yield actions.gameDidInitialize({
				...initialGameState,
				...initialData,
				rand: srand(getRandomSeed()),
				ready: true,
				animals: game.advanced
					? toggleTeacherBot(draftAnimals, true)
					: toggleTeacherBot(draftAnimals, false)
			})
		}

		const gameState = {...initialGameState, ...game, ...savedGame}
		const animals = gameState.animals.some(a => a.type === 'teacherBot')
			? gameState.animals
			: gameState.animals.concat(addTeacherBot(gameState.levelSize))

		if (gameState.advanced && !gameState.solution) {
			return yield context.setUrl('/')
		}

		return yield actions.gameDidInitialize({
			...gameState,
			solutionIterator: gameState.advanced
				? getIterator(gameState.solution[0].sequence, createApi(game.capabilities, state.active, game.palette))
				: null,
			painted: !props.game.advanced && gameState.initialPainted,
			rand: srand(getRandomSeed()),
			ready: true,
			animals: gameState.advanced
				? toggleTeacherBot(animals, true)
				: toggleTeacherBot(animals, false)
		})
  },

  render ({props, state, actions, context, children}) {
    const {savedGame = {}, game, gameRef, isDraft, saveRef, playlist, initialData, saved, userProfile, incrementAttempts, incrementSlowdowns} = props
		const type = game.type || 'write'
    const {isAnonymous} = context
		const {ready, docs} = state
    const userAnimal = isAnonymous ? 'penguin' : userProfile.bot

		if (!ready) {
			return
		}

    switch (type) {
      case 'read':
        return <ReadingChallenge
          {...state}
          {...actions}
          incrementTimeElapsed={props.incrementTimeElapsed}
          docs={docs}
          saved={saved}
          isDraft={isDraft}
          runnerActions={actions}
          game={isDraft ? initialData : game}
          savedGame={savedGame.value}
          userAnimal={userAnimal}
          gameRef={gameRef}
          saveRef={saveRef}
          playlist={playlist} />
      case 'write':
        return <WritingChallenge
          {...state}
          {...actions}
          docs={docs}
          saved={saved}
          gameActions={actions}
          gameRef={gameRef}
          userAnimal={userAnimal}
          saveRef={saveRef}
          playlist={playlist}
          isDraft={isDraft}
          draftData={initialData} />
      case 'project':
        return <WritingChallenge
          {...state}
          {...actions}
          saved={saved}
          isProject
          docs={docs}
          gameActions={actions}
          game={game.value}
          savedGame={savedGame.value}
          userAnimal={userAnimal}
          gameRef={gameRef}
          saveRef={saveRef}
          playlist={playlist}
          isDraft={isDraft}
          draftData={initialData} />
      default:
        return <WritingChallenge
          {...state}
          {...actions}
          docs={docs}
          saved={saved}
          gameActions={actions}
          gameRef={gameRef}
          userAnimal={userAnimal}
          saveRef={saveRef}
          playlist={playlist}
          isDraft={isDraft}
          draftData={initialData} />
    }
	},

  * onUpdate (prev, {props, state, actions}) {
		if (!state.ready) return

		if (prev.state.ready !== state.ready) {
			yield actions.reset()
		}

    if(prev.state.speed > state.speed) {
      yield props.incrementSlowdowns()
    }

		if (prev.state.animals && prev.state.animals.some(({sequence}, i) => sequence !== state.animals[i].sequence) || prev.state.targetPainted !== state.targetPainted) {
      if (!props.isDraft) {
        yield props.setSaved(false)
        yield props.save({
          animals: state.animals,
          targetPainted: state.targetPainted
        })
      }
      if (props.updateGame && props.game.advanced) {
        yield props.updateGame({
          solution: state.animals.slice(0, -1)
        })
      }
    }

    if (prev.state.animals && !isEqualSequence(prev.state.animals, state.animals)) {
      yield actions.setCompleted(true)
    }

		if (state.hasRun && !prev.state.running && state.running) {
			yield actions.run()
		}
	},

  * onRemove ({props, state}) {
    if (props.updateGame && props.game.advanced) {
      yield props.updateGame({
        solution: state.animals.slice(0, -1)
      })
    }
  },

	controller: {
		* onComplete ({props, state, context}, data) {
			if (!props.isDraft) {
				yield props.onComplete(data)
			} else {
        yield context.closeModal()
      }
		},
		* createRunners ({actions, props, state}, animals) {
			try {
				const {game} = props
				const {active} = state

				return yield animals.map(animal => createIteratorQ(getIterator(animal.sequence, createApi(game.capabilities, active, game.palette))))
			} catch (e) {
				yield actions.throwError(e)
				yield actions.onRunFinish()
			}
		},
    * throwError ({actions, context}, e) {
      const {lineNum, message} = e
      const errorLine = typeof lineNum === 'number'
        ? lineNum
        : stackTrace.parse(e.e ? e.e : e)[0].lineNumber - 5

      yield context.openModal({
        type: 'error',
        body: `${e.message}. Check the code at line ${errorLine + 1}.`,
        header: 'Error'
      })
      yield actions.onRunFinish()
    },
    * onRunFinish ({actions}) {
      yield actions.setRunning(false)
      yield actions.setCompleted(true)
    },
    * runCode ({actions, state, context, props}) {
      const {initialPainted, animals, running, completed, hasRun} = state

      if (running) {
        return yield actions.setRunning(false)
      } else if (completed || !hasRun) {
        yield actions.setHasRun(false)
        yield actions.setRunning(false)
        yield sleep(50)
        const its = yield actions.createRunners(
					animals.filter(a => a.type !== 'teacherBot')
				)
        yield actions.resetBoard({
          animals: animals.map(a => ({...a, current: a.initial})),
          painted: initialPainted
        })
        yield sleep(100)
        if (its) {
          yield actions.setRunning(true)
          yield actions.setRunners(its)
          yield props.incrementRuns()
        }
      } else {
        yield actions.setRunning(true)
      }
    },
    * run ({actions, state}) {
      const {pauseState, runners} = state
      pauseState
				? yield mapValues(({it, i, returnValue}) => actions.runBot(it, i, returnValue), pauseState)
				: yield runners.map((it, i) => actions.runBot(it, i))
    },
    * runBot ({actions, state}, it, i, returnValue) {
      const {running, animals, speed} = state

      if (!running) {
        return yield actions.setPauseState({it, i, returnValue})
      }

      const interval = getInterval(animals[i], speed)
      const args = yield actions.step(it, i, returnValue)

      if (args) {
        yield actions.incrementSteps()
        yield sleep(interval)
        yield actions.runBot(...args)
      } else {
        yield actions.onRunFinish()
      }
    },
    * runTeacherBot ({actions, state}, it, i, returnValue) {
      const interval = getInterval(null, state.speed)
      const args = yield actions.step(it, i, returnValue)

      if (args) {
        yield sleep(interval)
        yield actions.runTeacherBot(...args)
      } else {
        yield actions.setInitialPainted(state.painted)
        yield sleep(500)
        yield actions.removeTeacherBot()
        yield sleep(100)
        yield actions.animalMove(
          state.animals.length - 1,
          state.animals[state.animals.length - 1].initial.location
        )
      }
    },
    * stepForward ({props, state, actions}) {
      const {completed, hasRun, animals, pauseState, stepping, initialPainted} = state

      if (stepping) {
        return
      }

      yield props.incrementStepperSteps()
      yield actions.setStepping(true)

      if (!hasRun) {
        yield actions.resetBoard({
          animals: animals.map(a => ({...a, current: a.initial})),
          painted: initialPainted
        })

        const newRunners = yield actions.createRunners(
					animals.filter(a => a.type !== 'teacherBot')
				)
        if (!newRunners) {
          return
        }

        const its = yield newRunners.map((it, i) => actions.step(it, i))
        yield its.map(([it, i, returnValue]) => actions.setPauseState({it, i, returnValue}))
        yield actions.setHasRun(true)
      } else {
        const its = yield mapValues(({it, i, returnValue}) => actions.step(it, i, returnValue), pauseState)
        if (Object.keys(its.filter(res => !!res)).length < 1) {
          yield actions.onRunFinish()
        } else {
          yield its.map(([it, i, returnValue]) => actions.setPauseState({it, i, returnValue}))
        }
      }

      yield actions.setStepping(false)
    },
    * step ({state, actions}, it, i, returnValue) {
      const {value, done} = it.next(returnValue)

      if (done) {
        return
      }

      const newReturnValue = yield actions.handleStepAction(value)
      return [it, i, newReturnValue]
    },

    * handleStepAction ({actions, state, props}, action) {
      const [lineNum, ...args] = action.payload && action.payload

      if (action.type === 'throwError') {
        yield actions.throwError(...action.payload)
        return
      }

      const [frame, result] = frameReducer(state, action.type, args)

      if (!checkBounds(frame.animals[frame.active].current.location, state.levelSize)) {
        yield actions.throwError({message: 'Out of bounds', lineNum: lineNum - 1})
        return result
      } else {
        yield actions.setFrame(frame)
      }

      if (!isNaN(lineNum) && lineNum >= 0) {
        yield actions.setActiveLine(lineNum)
      }

      return result
    },

    * startOver ({state, actions}) {
      yield actions.gameDidInitialize(state.initialData)
    },
    * reset ({state, actions, props}) {
      const {animals, advanced, initialData, randSeeds, targetPainted} = state
      const teacherCode = initialData.initialPainted
      const newState = {
				rand: srand(getRandomSeed(randSeeds)),
        animals: animals.map(animal => ({
          ...animal,
          current: animal.initial
        })),
        pauseState: undefined,
				completed: false,
        activeLine: -1,
        running: false,
				startGrid: {},
        hasRun: false,
        painted: advanced ? {} : initialData.initialPainted,
        targetPainted: advanced ? {} : targetPainted,
        runners: {},
        paints: 0,
        steps: 0
      }

      yield actions.resetBoard(newState)

      if (advanced && typeof teacherCode === 'string') {
        yield actions.addTeacherBot()
        yield sleep(500)
        const startCode = getIterator(
          teacherCode,
          createApi(teacherBot, animals.length - 1)
        )
        const it = createIterator(startCode)
        yield actions.runTeacherBot(it, animals.length - 1)
        yield actions.getAdvancedPainted()
      }
    },
    * getAdvancedPainted ({state, actions}) {
      const {solution, capabilities, palette} = state
      if (solution) {
        yield actions.setTargetPainted(getLastFrame(
          state,
          getIterator(solution[0].sequence, createApi(capabilities, 0, palette))
        ))
      }
    }
  },

  reducer: {
    resetBoard: (state, initState) => ({
      ...initState,
      pauseState: null,
      steps: 0,
      completed: false,
      hasRun: false,
      activeLine: -1
    }),
    addTeacherBot: (state) => ({
      animals: state.animals.map(a => {
        return a.type === 'teacherBot'
					? {...a, hidden: false}
					: {...a, hidden: true}
      })
    }),
    removeTeacherBot: (state) => ({
      activeLine: -1,
      animals: state.animals.map(a => {
        return a.type === 'teacherBot'
					? {...a, hidden: true}
					: {...a, hidden: false}
      })
    }),
    setTargetPainted: (state, targetPainted) => ({targetPainted}),
    setFrame: (state, frame) => frame,
    setInitialPainted: (state, initialPainted) => ({initialPainted, startGrid: initialPainted}),
    setSequence: ({animals, active}, sequence) => ({
      animals: updateAnimal(animals, 'sequence', active, sequence)
    }),
    gameDidInitialize: (state, game) => ({
			...game,
      initialData: game
    }),
    setPainted: (state, grid, coord, color) => ({
      [grid]: {...state[grid], [coord]: color},
      paints: (state.paints || 0) + 1
    }),
    animalMove: setAnimalPos,
    animalTurn: turn,
    setRandSeeds: (state, randSeeds) => ({randSeeds}),
    setSolutionChecker: (state, solutionChecker) => ({solutionChecker}),
    setCompleted: (state, completed) => ({completed}),
    setLocalDescription: (state, description) => ({description}),
    incrementSteps: (state) => ({steps: state.steps + 1}),
    setRunners: (state, runners) => ({runners, hasRun: true}),
    setPauseState: (state, payload) => setProp(`pauseState.${payload.i}`, state, {...payload}),
    setActive: (state, {id}) => ({active: id}),
    setSpeed: (state, speed) => ({
      speed: Math.max(0.25, Math.min(speed, 16))
    }),
		setReady: () => ({ready: true}),
    setStepping: (state, stepping) => ({stepping}),
    setHasRun: (state, hasRun) => ({hasRun}),
    setRunning: (state, running) => ({running, hasRun: true}),
    setActiveLine: (state, activeLine) => ({activeLine})
  }
})

function getRandomSeed (seeds) {
  if (!seeds || seeds.length === 0) return Math.random() * 1000

  return seeds[Math.floor(Math.random() * seeds.length)]
}
<commit_msg>fix paint saves<commit_after>/**
 * Imports
 */

import frameReducer, {
  createIteratorQ, checkBounds, isEqualSequence, updateAnimal,
  createIterator, getIterator, setAnimalPos, turn, getLastFrame
} from 'utils/frameReducer'
import createApi, {teacherBot, createDocs} from 'animalApis'
import ReadingChallenge from 'components/ReadingChallenge'
import WritingChallenge from 'components/WritingChallenge'
import {getInterval, resetAnimalPos} from 'utils/animal'
import initialGameState from 'utils/initialGameState'
import {component, element} from 'vdux'
import mapValues from '@f/map-values'
import stackTrace from 'stack-trace'
import setProp from '@f/set-prop'
import sleep from '@f/sleep'
import srand from '@f/srand'

/**
 * <Game/>
 */

function addTeacherBot (levelSize) {
  return {
    type: 'teacherBot',
    current: {
      location: [levelSize[0] - 1, 0],
      rot: 0
    },
    initial: {
      location: [levelSize[0] - 1, 0],
      rot: 0
    },
    hidden: false
  }
}

function toggleTeacherBot (animals, toggle) {
  return animals.map(a => {
    return a.type !== 'teacherBot'
			? {...a, hidden: toggle}
			: {...a, hidden: !toggle}
  })
}

export default component({
	initialState: ({props}) => ({
		ready: false,
    docs: createDocs(props.game.capabilities, props.game.palette)
	}),

  * onCreate ({state, actions, context, props}) {
		const {game, savedGame = {}, initialData = {}} = props
		if (Object.keys(initialData).length > 0) {
			const draftAnimals = initialData.advanced && initialData.solution
				? initialData.solution.concat(addTeacherBot(game.levelSize))
				: initialData.animals.map(a => resetAnimalPos(a, game.levelSize[0])).concat(addTeacherBot(game.levelSize))
			return yield actions.gameDidInitialize({
				...initialGameState,
				...initialData,
				rand: srand(getRandomSeed()),
				ready: true,
				animals: game.advanced
					? toggleTeacherBot(draftAnimals, true)
					: toggleTeacherBot(draftAnimals, false)
			})
		}

		const gameState = {...initialGameState, ...game, ...savedGame}
		const animals = gameState.animals.some(a => a.type === 'teacherBot')
			? gameState.animals
			: gameState.animals.concat(addTeacherBot(gameState.levelSize))

		if (gameState.advanced && !gameState.solution) {
			return yield context.setUrl('/')
		}

		return yield actions.gameDidInitialize({
			...gameState,
			solutionIterator: gameState.advanced
				? getIterator(gameState.solution[0].sequence, createApi(game.capabilities, state.active, game.palette))
				: null,
			painted: !props.game.advanced && gameState.initialPainted,
			rand: srand(getRandomSeed()),
			ready: true,
			animals: gameState.advanced
				? toggleTeacherBot(animals, true)
				: toggleTeacherBot(animals, false)
		})
  },

  render ({props, state, actions, context, children}) {
    const {savedGame = {}, game, gameRef, isDraft, saveRef, playlist, initialData, saved, userProfile, incrementAttempts, incrementSlowdowns} = props
		const type = game.type || 'write'
    const {isAnonymous} = context
		const {ready, docs} = state
    const userAnimal = isAnonymous ? 'penguin' : userProfile.bot

		if (!ready) {
			return
		}

    switch (type) {
      case 'read':
        return <ReadingChallenge
          {...state}
          {...actions}
          incrementTimeElapsed={props.incrementTimeElapsed}
          docs={docs}
          saved={saved}
          isDraft={isDraft}
          runnerActions={actions}
          game={isDraft ? initialData : game}
          savedGame={savedGame.value}
          userAnimal={userAnimal}
          gameRef={gameRef}
          saveRef={saveRef}
          playlist={playlist} />
      case 'write':
        return <WritingChallenge
          {...state}
          {...actions}
          docs={docs}
          saved={saved}
          gameActions={actions}
          gameRef={gameRef}
          userAnimal={userAnimal}
          saveRef={saveRef}
          playlist={playlist}
          isDraft={isDraft}
          draftData={initialData} />
      case 'project':
        return <WritingChallenge
          {...state}
          {...actions}
          saved={saved}
          isProject
          docs={docs}
          gameActions={actions}
          game={game.value}
          savedGame={savedGame.value}
          userAnimal={userAnimal}
          gameRef={gameRef}
          saveRef={saveRef}
          playlist={playlist}
          isDraft={isDraft}
          draftData={initialData} />
      default:
        return <WritingChallenge
          {...state}
          {...actions}
          docs={docs}
          saved={saved}
          gameActions={actions}
          gameRef={gameRef}
          userAnimal={userAnimal}
          saveRef={saveRef}
          playlist={playlist}
          isDraft={isDraft}
          draftData={initialData} />
    }
	},

  * onUpdate (prev, {props, state, actions}) {
		if (!state.ready) return

		if (prev.state.ready !== state.ready) {
			yield actions.reset()
		}

    if(prev.state.speed > state.speed) {
      yield props.incrementSlowdowns()
    }

		if (prev.state.animals && prev.state.animals.some(({sequence}, i) => sequence !== state.animals[i].sequence)) {
      if (!props.isDraft) {
        yield props.setSaved(false)
        yield props.save({animals: state.animals})
      }
      if (props.updateGame && props.game.advanced) {
        yield props.updateGame({
          solution: state.animals.slice(0, -1)
        })
      }
    }

    if (prev.state.targetPainted !== state.targetPainted && !props.isDraft) {
      yield props.save({targetPainted: state.targetPainted})
    }

    if (prev.state.animals && !isEqualSequence(prev.state.animals, state.animals)) {
      yield actions.setCompleted(true)
    }

		if (state.hasRun && !prev.state.running && state.running) {
			yield actions.run()
		}
	},

  * onRemove ({props, state}) {
    if (props.updateGame && props.game.advanced) {
      yield props.updateGame({
        solution: state.animals.slice(0, -1)
      })
    }
  },

	controller: {
		* onComplete ({props, state, context}, data) {
			if (!props.isDraft) {
				yield props.onComplete(data)
			} else {
        yield context.closeModal()
      }
		},
		* createRunners ({actions, props, state}, animals) {
			try {
				const {game} = props
				const {active} = state

				return yield animals.map(animal => createIteratorQ(getIterator(animal.sequence, createApi(game.capabilities, active, game.palette))))
			} catch (e) {
				yield actions.throwError(e)
				yield actions.onRunFinish()
			}
		},
    * throwError ({actions, context}, e) {
      const {lineNum, message} = e
      const errorLine = typeof lineNum === 'number'
        ? lineNum
        : stackTrace.parse(e.e ? e.e : e)[0].lineNumber - 5

      yield context.openModal({
        type: 'error',
        body: `${e.message}. Check the code at line ${errorLine + 1}.`,
        header: 'Error'
      })
      yield actions.onRunFinish()
    },
    * onRunFinish ({actions}) {
      yield actions.setRunning(false)
      yield actions.setCompleted(true)
    },
    * runCode ({actions, state, context, props}) {
      const {initialPainted, animals, running, completed, hasRun} = state

      if (running) {
        return yield actions.setRunning(false)
      } else if (completed || !hasRun) {
        yield actions.setHasRun(false)
        yield actions.setRunning(false)
        yield sleep(50)
        const its = yield actions.createRunners(
					animals.filter(a => a.type !== 'teacherBot')
				)
        yield actions.resetBoard({
          animals: animals.map(a => ({...a, current: a.initial})),
          painted: initialPainted
        })
        yield sleep(100)
        if (its) {
          yield actions.setRunning(true)
          yield actions.setRunners(its)
          yield props.incrementRuns()
        }
      } else {
        yield actions.setRunning(true)
      }
    },
    * run ({actions, state}) {
      const {pauseState, runners} = state
      pauseState
				? yield mapValues(({it, i, returnValue}) => actions.runBot(it, i, returnValue), pauseState)
				: yield runners.map((it, i) => actions.runBot(it, i))
    },
    * runBot ({actions, state}, it, i, returnValue) {
      const {running, animals, speed} = state

      if (!running) {
        return yield actions.setPauseState({it, i, returnValue})
      }

      const interval = getInterval(animals[i], speed)
      const args = yield actions.step(it, i, returnValue)

      if (args) {
        yield actions.incrementSteps()
        yield sleep(interval)
        yield actions.runBot(...args)
      } else {
        yield actions.onRunFinish()
      }
    },
    * runTeacherBot ({actions, state}, it, i, returnValue) {
      const interval = getInterval(null, state.speed)
      const args = yield actions.step(it, i, returnValue)

      if (args) {
        yield sleep(interval)
        yield actions.runTeacherBot(...args)
      } else {
        yield actions.setInitialPainted(state.painted)
        yield sleep(500)
        yield actions.removeTeacherBot()
        yield sleep(100)
        yield actions.animalMove(
          state.animals.length - 1,
          state.animals[state.animals.length - 1].initial.location
        )
      }
    },
    * stepForward ({props, state, actions}) {
      const {completed, hasRun, animals, pauseState, stepping, initialPainted} = state

      if (stepping) {
        return
      }

      yield props.incrementStepperSteps()
      yield actions.setStepping(true)

      if (!hasRun) {
        yield actions.resetBoard({
          animals: animals.map(a => ({...a, current: a.initial})),
          painted: initialPainted
        })

        const newRunners = yield actions.createRunners(
					animals.filter(a => a.type !== 'teacherBot')
				)
        if (!newRunners) {
          return
        }

        const its = yield newRunners.map((it, i) => actions.step(it, i))
        yield its.map(([it, i, returnValue]) => actions.setPauseState({it, i, returnValue}))
        yield actions.setHasRun(true)
      } else {
        const its = yield mapValues(({it, i, returnValue}) => actions.step(it, i, returnValue), pauseState)
        if (Object.keys(its.filter(res => !!res)).length < 1) {
          yield actions.onRunFinish()
        } else {
          yield its.map(([it, i, returnValue]) => actions.setPauseState({it, i, returnValue}))
        }
      }

      yield actions.setStepping(false)
    },
    * step ({state, actions}, it, i, returnValue) {
      const {value, done} = it.next(returnValue)

      if (done) {
        return
      }

      const newReturnValue = yield actions.handleStepAction(value)
      return [it, i, newReturnValue]
    },

    * handleStepAction ({actions, state, props}, action) {
      const [lineNum, ...args] = action.payload && action.payload

      if (action.type === 'throwError') {
        yield actions.throwError(...action.payload)
        return
      }

      const [frame, result] = frameReducer(state, action.type, args)

      if (!checkBounds(frame.animals[frame.active].current.location, state.levelSize)) {
        yield actions.throwError({message: 'Out of bounds', lineNum: lineNum - 1})
        return result
      } else {
        yield actions.setFrame(frame)
      }

      if (!isNaN(lineNum) && lineNum >= 0) {
        yield actions.setActiveLine(lineNum)
      }

      return result
    },

    * startOver ({state, actions}) {
      yield actions.gameDidInitialize(state.initialData)
    },
    * reset ({state, actions, props}) {
      const {animals, advanced, initialData, randSeeds, targetPainted} = state
      const teacherCode = initialData.initialPainted
      const newState = {
				rand: srand(getRandomSeed(randSeeds)),
        animals: animals.map(animal => ({
          ...animal,
          current: animal.initial
        })),
        pauseState: undefined,
				completed: false,
        activeLine: -1,
        running: false,
				startGrid: {},
        hasRun: false,
        painted: advanced ? {} : initialData.initialPainted,
        targetPainted: advanced ? {} : targetPainted,
        runners: {},
        paints: 0,
        steps: 0
      }

      yield actions.resetBoard(newState)

      if (advanced && typeof teacherCode === 'string') {
        yield actions.addTeacherBot()
        yield sleep(500)
        const startCode = getIterator(
          teacherCode,
          createApi(teacherBot, animals.length - 1)
        )
        const it = createIterator(startCode)
        yield actions.runTeacherBot(it, animals.length - 1)
        yield actions.getAdvancedPainted()
      }
    },
    * getAdvancedPainted ({state, actions}) {
      const {solution, capabilities, palette} = state
      if (solution) {
        yield actions.setTargetPainted(getLastFrame(
          state,
          getIterator(solution[0].sequence, createApi(capabilities, 0, palette))
        ))
      }
    }
  },

  reducer: {
    resetBoard: (state, initState) => ({
      ...initState,
      pauseState: null,
      steps: 0,
      completed: false,
      hasRun: false,
      activeLine: -1
    }),
    addTeacherBot: (state) => ({
      animals: state.animals.map(a => {
        return a.type === 'teacherBot'
					? {...a, hidden: false}
					: {...a, hidden: true}
      })
    }),
    removeTeacherBot: (state) => ({
      activeLine: -1,
      animals: state.animals.map(a => {
        return a.type === 'teacherBot'
					? {...a, hidden: true}
					: {...a, hidden: false}
      })
    }),
    setTargetPainted: (state, targetPainted) => ({targetPainted}),
    setFrame: (state, frame) => frame,
    setInitialPainted: (state, initialPainted) => ({initialPainted, startGrid: initialPainted}),
    setSequence: ({animals, active}, sequence) => ({
      animals: updateAnimal(animals, 'sequence', active, sequence)
    }),
    gameDidInitialize: (state, game) => ({
			...game,
      initialData: game
    }),
    setPainted: (state, grid, coord, color) => ({
      [grid]: {...state[grid], [coord]: color},
      paints: (state.paints || 0) + 1
    }),
    animalMove: setAnimalPos,
    animalTurn: turn,
    setRandSeeds: (state, randSeeds) => ({randSeeds}),
    setSolutionChecker: (state, solutionChecker) => ({solutionChecker}),
    setCompleted: (state, completed) => ({completed}),
    setLocalDescription: (state, description) => ({description}),
    incrementSteps: (state) => ({steps: state.steps + 1}),
    setRunners: (state, runners) => ({runners, hasRun: true}),
    setPauseState: (state, payload) => setProp(`pauseState.${payload.i}`, state, {...payload}),
    setActive: (state, {id}) => ({active: id}),
    setSpeed: (state, speed) => ({
      speed: Math.max(0.25, Math.min(speed, 16))
    }),
		setReady: () => ({ready: true}),
    setStepping: (state, stepping) => ({stepping}),
    setHasRun: (state, hasRun) => ({hasRun}),
    setRunning: (state, running) => ({running, hasRun: true}),
    setActiveLine: (state, activeLine) => ({activeLine})
  }
})

function getRandomSeed (seeds) {
  if (!seeds || seeds.length === 0) return Math.random() * 1000

  return seeds[Math.floor(Math.random() * seeds.length)]
}
<|endoftext|>


<commit_before>"use babel";

let {$, ScrollView} = require('atom-space-pen-views');
let {Point, TextEditor} = require('atom');
let fs = require('fs-plus');
let path = require('path');
let _ = require('underscore-plus');
let {File, Disposable, CompositeDisposable} = require('atom');
let {Function} = require('loophole');
global.Function = Function;

global.PDFJS = {workerSrc: "temp", cMapUrl:"temp", cMapPacked:true};
require('./../node_modules/pdfjs-dist/build/pdf.js');
PDFJS.workerSrc = "file://" + path.resolve(__dirname, "../node_modules/pdfjs-dist/build/pdf.worker.js");
PDFJS.cMapUrl = "file://" + path.resolve(__dirname, "../node_modules/pdfjs-dist/cmaps")+"/";

let {exec, execFile} = require('child_process');

export default class PdfEditorView extends ScrollView {
  static content() {
    this.div({class: 'pdf-view', tabindex: -1}, () => {
      this.div({outlet: 'container'});
    });
  }

  constructor(filePath) {
    super();

    this.currentScale = 1.5;
    this.defaultScale = 1.5;
    this.scaleFactor = 10.0;

    this.filePath = filePath;
    this.file = new File(this.filePath);
    this.canvases = [];

    this.updatePdf();

    this.currentPageNumber = 0;
    this.totalPageNumber = 0;
    this.centersBetweenPages = [];
    this.pageHeights = [];
    this.scrollTopBeforeUpdate = 0;
    this.scrollLeftBeforeUpdate = 0;
    this.updating = false;

    let disposables = new CompositeDisposable();

    let onFileChangeCallback = _.debounce(() => {
      if (this.updating) {
        this.fileChanged = true;
      } else {
        this.updatePdf();
      }
    }, 100);

    disposables.add(this.file.onDidChange(onFileChangeCallback));

    let moveLeftCallback = (() => this.scrollLeft(this.scrollLeft() - $(window).width() / 20));
    let moveRightCallback = (() => this.scrollRight(this.scrollRight() + $(window).width() / 20));
    let scrollCallback = (() => this.onScroll());
    let resizeHandler = (() => this.setCurrentPageNumber());

    let elem = this;

    atom.commands.add('.pdf-view', {
      'core:move-left': moveLeftCallback,
      'core:move-right': moveRightCallback
    });

    elem.on('scroll', scrollCallback);
    disposables.add(new Disposable(() => $(window).off('scroll', scrollCallback)));

    $(window).on('resize', resizeHandler);
    disposables.add(new Disposable(() => $(window).off('resize', resizeHandler)));

    atom.commands.add('atom-workspace', {
      'pdf-view:zoom-in': () => {
        if (atom.workspace.getActivePaneItem() === this) {
          this.zoomIn();
        }
      },
      'pdf-view:zoom-out': () => {
        if (atom.workspace.getActivePaneItem() === this) {
          this.zoomOut();
        }
      },
      'pdf-view:reset-zoom': () => {
        if (atom.workspace.getActivePaneItem() === this) {
          this.resetZoom();
        }
      }
    });

    this.dragging = null;

    this.onMouseMove = (e) => {
      if (this.dragging) {
        this.simpleClick = false;

        this.scrollTop(this.dragging.scrollTop - (e.screenY - this.dragging.y));
        this.scrollLeft(this.dragging.scrollLeft - (e.screenX - this.dragging.x));
        e.preventDefault();
      }
    };

    this.onMouseUp = (e) => {
      this.dragging = null;
      $(document).unbind('mousemove', this.onMouseMove);
      $(document).unbind('mouseup', this.onMouseUp);
      e.preventDefault();
    };

    this.on('mousedown', (e) => {
      this.simpleClick = true;
      atom.workspace.paneForItem(this).activate();
      this.dragging = {x: e.screenX, y: e.screenY, scrollTop: this.scrollTop(), scrollLeft: this.scrollLeft()};
      $(document).on('mousemove', this.onMouseMove);
      $(document).on('mouseup', this.onMouseUp);
      e.preventDefault();
    });

    this.on('mousewheel', (e) => {
      if (e.ctrlKey) {
        e.preventDefault();
        if (e.originalEvent.wheelDelta > 0) {
          this.zoomIn();
        } else if (e.originalEvent.wheelDelta < 0) {
          this.zoomOut();
        }
      }
    });
  }

  onCanvasClick(page, e) {
    if (this.simpleClick && atom.config.get('pdf-view.enableSyncTeX')) {
      e.preventDefault();
      this.pdfDocument.getPage(page).then((pdfPage) => {
        let viewport = pdfPage.getViewport(this.currentScale);
        [x,y] = viewport.convertToPdfPoint(e.offsetX, $(this.canvases[page - 1]).height() - e.offsetY);

        let callback = ((error, stdout, stderr) => {
          if (!error) {
            stdout = stdout.replace(/\r\n/g, '\n');
            let attrs = {};
            for (let line of stdout.split('\n')) {
              let m = line.match(/^([a-zA-Z]*):(.*)$/)
              if (m) {
                attrs[m[1]] = m[2];
              }
            }

            let file = attrs.Input;
            let line = attrs.Line;

            if (file && line) {
              let editor = null;
              let pathToOpen = path.normalize(attrs.Input);
              let lineToOpen = +attrs.Line;
              let done = false;
              for (let editor of atom.workspace.getTextEditors()) {
                if (editor.getPath() === pathToOpen) {
                  let position = new Point(lineToOpen-1, -1);
                  editor.scrollToBufferPosition(position, {center: true});
                  editor.setCursorBufferPosition(position);
                  editor.moveToFirstCharacterOfLine();
                  let pane = atom.workspace.paneForItem(editor);
                  pane.activateItem(editor);
                  pane.activate();
                  done = true;
                  break;
                }
              }

              if (!done) {
                atom.workspace.open(pathToOpen, {initialLine: lineToOpen, initialColumn: 0})
              }
            }
          }
        });

        let synctexPath = atom.config.get('pdf-view.syncTeXPath');
        let clickspec = [page, x, y, this.filePath].join(':');

        if (synctexPath) {
          execFile(synctexPath, ["edit", "-o", clickspec], callback);
        } else {
          let cmd = `synctex edit -o "${clickspec}"`;
          exec(cmd, callback);
        }
      });
    }
  }

  onScroll() {
    if (!this.updating) {
      this.scrollTopBeforeUpdate = this.scrollTop();
      this.scrollLeftBeforeUpdate = this.scrollLeft();
    }

    this.setCurrentPageNumber();
  }

  setCurrentPageNumber() {
    if (!this.pdfDocument) {
      return;
    }

    let center = (this.scrollBottom() + this.scrollTop())/2.0
    this.currentPageNumber = 1

    if (this.centersBetweenPages.length === 0 && this.pageHeights.length === this.pdfDocument.numPages)
      for (let pdfPageNumber of _.range(1, this.pdfDocument.numPages+1)) {
        this.centersBetweenPages.push(this.pageHeights.slice(0, pdfPageNumber).reduce(((x,y) => x + y), 0) + pdfPageNumber * 20 - 10);
      }

    for (let pdfPageNumber of _.range(2, this.pdfDocument.numPages+1)) {
      if (center >= this.centersBetweenPages[pdfPageNumber-2] && center < this.centersBetweenPages[pdfPageNumber-1]) {
        this.currentPageNumber = pdfPageNumber;
      }
    }

    atom.views.getView(atom.workspace).dispatchEvent(new Event('pdf-view:current-page-update'));
  }

  finishUpdate() {
    this.updating = false;
    if (this.fileChanged) {
      this.updatePdf();
    }
  }

  updatePdf() {
    this.fileChanged = false;

    if (!fs.existsSync(this.filePath)) {
      return;
    }

    let pdfData = null;

    try {
      pdfData = new Uint8Array(fs.readFileSync(this.filePath));
    } catch (error) {
      if (error.code === 'ENOENT') {
        return;
      }
    }

    this.updating = true;
    this.container.find("canvas").remove();
    this.canvases = [];

    PDFJS.getDocument(pdfData).then((pdfDocument) => {
      this.pdfDocument = pdfDocument;
      this.totalPageNumber = this.pdfDocument.numPages;

      for (let pdfPageNumber of _.range(1, this.pdfDocument.numPages+1)) {
        let canvas = $("<canvas/>", {class: "page-container"}).appendTo(this.container)[0];
        this.canvases.push(canvas);
        $(canvas).on('click', (e) => this.onCanvasClick(pdfPageNumber, e));
      }

      this.renderPdf();
    }, () => this.finishUpdate());
  }

  renderPdf(scrollAfterRender = true) {
    this.centersBetweenPages = [];
    this.pageHeights = [];

    for (let pdfPageNumber of _.range(1, this.pdfDocument.numPages+1)) {
      let canvas = this.canvases[pdfPageNumber-1];

      this.pdfDocument.getPage(pdfPageNumber).then((pdfPage) => {
        let viewport = pdfPage.getViewport(this.currentScale);
        let context = canvas.getContext('2d');

        let outputScale = window.devicePixelRatio;
        canvas.height = Math.floor(viewport.height) * outputScale;
        canvas.width = Math.floor(viewport.width) * outputScale;

        if (outputScale != 1) {
          context._scaleX = outputScale;
          context._scaleY = outputScale;
          context.scale(outputScale, outputScale);
          canvas.style.width = Math.floor(viewport.width) + 'px';
          canvas.style.height = Math.floor(viewport.height) + 'px';
        }

        this.pageHeights.push(Math.floor(viewport.height));

        pdfPage.render({canvasContext: context, viewport: viewport});

        if (pdfPage.pageNumber === this.pdfDocument.numPages && scrollAfterRender) {
          this.scrollTop(this.scrollTopBeforeUpdate);
          this.scrollLeft(this.scrollLeftBeforeUpdate);
          this.setCurrentPageNumber();
          this.finishUpdate();
        }
      }, () => this.finishUpdate());
    }
  }

  zoomOut() {
    return this.adjustSize(100 / (100 + this.scaleFactor));
  }

  zoomIn() {
    return this.adjustSize((100 + this.scaleFactor) / 100);
  }

  resetZoom() {
    return this.adjustSize(this.defaultScale / this.currentScale);
  }

  computeZoomedScrollTop(oldScrollTop, oldPageHeights) {
    let pixelsToZoom = 0;
    let spacesToSkip = 0;
    let zoomedPixels = 0;

    for (let pdfPageNumber of _.range(0, this.pdfDocument.numPages)) {
      if (pixelsToZoom + spacesToSkip + oldPageHeights[pdfPageNumber] > oldScrollTop) {
        zoomFactorForPage = this.pageHeights[pdfPageNumber] / oldPageHeights[pdfPageNumber];
        let partOfPageAboveUpperBorder = oldScrollTop - (pixelsToZoom + spacesToSkip);
        zoomedPixels += Math.round(partOfPageAboveUpperBorder * zoomFactorForPage);
        pixelsToZoom += partOfPageAboveUpperBorder;
        break;
      } else {
        pixelsToZoom += oldPageHeights[pdfPageNumber];
        zoomedPixels += this.pageHeights[pdfPageNumber];
      }

      if (pixelsToZoom + spacesToSkip + 20 > oldScrollTop) {
        let partOfPaddingAboveUpperBorder = oldScrollTop - (pixelsToZoom + spacesToSkip);
        spacesToSkip += partOfPaddingAboveUpperBorder;
        break;
      } else {
        spacesToSkip += 20;
      }
    }

    return zoomedPixels + spacesToSkip;
  }

  adjustSize(factor) {
    if (!this.pdfDocument) {
      return;
    }

    let oldScrollTop = this.scrollTop();
    let oldPageHeights = this.pageHeights.slice(0);
    this.currentScale = this.currentScale * factor;
    this.renderPdf(false);

    process.nextTick(() => {
      let newScrollTop = this.computeZoomedScrollTop(oldScrollTop, oldPageHeights);
      this.scrollTop(newScrollTop);
    });

    process.nextTick(() => {
      let newScrollLeft = this.scrollLeft() * factor;
      this.scrollLeft(newScrollLeft);
    });
  }

  getCurrentPageNumber() {
    return this.currentPageNumber;
  }

  getTotalPageNumber() {
    return this.totalPageNumber;
  }

  scrollToPage(pdfPageNumber) {
    if (!this.pdfDocument || isNaN(pdfPageNumber)) {
      return;
    }

    pdfPageNumber = Math.min(pdfPageNumber, this.pdfDocument.numPages);
    pageScrollPosition = (this.pageHeights.slice(0, (pdfPageNumber-1)).reduce(((x,y) => x+y), 0)) + (pdfPageNumber - 1) * 20

    return this.scrollTop(pageScrollPosition);
  }

  serialize() {
    return {filePath: this.filePath, deserializer: 'PdfEditorDeserializer'};
  }

  getTitle() {
    if (this.filePath) {
      return path.basename(this.filePath);
    } else {
      return 'untitled';
    }
  }

  getURI() {
    return this.filePath;
  }

  getPath() {
    return this.filePath;
  }

  destroy() {
    return this.detach();
  }

  onDidChangeTitle() {
    return new Disposable(() => null);
  }

  onDidChangeModified() {
    return new Disposable(() => null);
  }
}
<commit_msg>Add SyncTeX forward sync support<commit_after>"use babel";

let {$, ScrollView} = require('atom-space-pen-views');
let {Point, TextEditor} = require('atom');
let fs = require('fs-plus');
let path = require('path');
let _ = require('underscore-plus');
let {File, Disposable, CompositeDisposable} = require('atom');
let {Function} = require('loophole');
global.Function = Function;

global.PDFJS = {workerSrc: "temp", cMapUrl:"temp", cMapPacked:true};
require('./../node_modules/pdfjs-dist/build/pdf.js');
PDFJS.workerSrc = "file://" + path.resolve(__dirname, "../node_modules/pdfjs-dist/build/pdf.worker.js");
PDFJS.cMapUrl = "file://" + path.resolve(__dirname, "../node_modules/pdfjs-dist/cmaps")+"/";

let {exec, execFile} = require('child_process');

export default class PdfEditorView extends ScrollView {
  static content() {
    this.div({class: 'pdf-view', tabindex: -1}, () => {
      this.div({outlet: 'container'});
    });
  }

  constructor(filePath) {
    super();

    this.currentScale = 1.5;
    this.defaultScale = 1.5;
    this.scaleFactor = 10.0;

    this.filePath = filePath;
    this.file = new File(this.filePath);
    this.canvases = [];

    this.updatePdf();

    this.currentPageNumber = 0;
    this.totalPageNumber = 0;
    this.centersBetweenPages = [];
    this.pageHeights = [];
    this.scrollTopBeforeUpdate = 0;
    this.scrollLeftBeforeUpdate = 0;
    this.updating = false;

    let disposables = new CompositeDisposable();

    let onFileChangeCallback = _.debounce(() => {
      if (this.updating) {
        this.fileChanged = true;
      } else {
        this.updatePdf();
      }
    }, 100);

    disposables.add(this.file.onDidChange(onFileChangeCallback));

    let moveLeftCallback = (() => this.scrollLeft(this.scrollLeft() - $(window).width() / 20));
    let moveRightCallback = (() => this.scrollRight(this.scrollRight() + $(window).width() / 20));
    let scrollCallback = (() => this.onScroll());
    let resizeHandler = (() => this.setCurrentPageNumber());

    let elem = this;

    atom.commands.add('.pdf-view', {
      'core:move-left': moveLeftCallback,
      'core:move-right': moveRightCallback
    });

    elem.on('scroll', scrollCallback);
    disposables.add(new Disposable(() => $(window).off('scroll', scrollCallback)));

    $(window).on('resize', resizeHandler);
    disposables.add(new Disposable(() => $(window).off('resize', resizeHandler)));

    atom.commands.add('atom-workspace', {
      'pdf-view:zoom-in': () => {
        if (atom.workspace.getActivePaneItem() === this) {
          this.zoomIn();
        }
      },
      'pdf-view:zoom-out': () => {
        if (atom.workspace.getActivePaneItem() === this) {
          this.zoomOut();
        }
      },
      'pdf-view:reset-zoom': () => {
        if (atom.workspace.getActivePaneItem() === this) {
          this.resetZoom();
        }
      }
    });

    this.dragging = null;

    this.onMouseMove = (e) => {
      if (this.dragging) {
        this.simpleClick = false;

        this.scrollTop(this.dragging.scrollTop - (e.screenY - this.dragging.y));
        this.scrollLeft(this.dragging.scrollLeft - (e.screenX - this.dragging.x));
        e.preventDefault();
      }
    };

    this.onMouseUp = (e) => {
      this.dragging = null;
      $(document).unbind('mousemove', this.onMouseMove);
      $(document).unbind('mouseup', this.onMouseUp);
      e.preventDefault();
    };

    this.on('mousedown', (e) => {
      this.simpleClick = true;
      atom.workspace.paneForItem(this).activate();
      this.dragging = {x: e.screenX, y: e.screenY, scrollTop: this.scrollTop(), scrollLeft: this.scrollLeft()};
      $(document).on('mousemove', this.onMouseMove);
      $(document).on('mouseup', this.onMouseUp);
      e.preventDefault();
    });

    this.on('mousewheel', (e) => {
      if (e.ctrlKey) {
        e.preventDefault();
        if (e.originalEvent.wheelDelta > 0) {
          this.zoomIn();
        } else if (e.originalEvent.wheelDelta < 0) {
          this.zoomOut();
        }
      }
    });
  }

  onCanvasClick(page, e) {
    if (this.simpleClick && atom.config.get('pdf-view.enableSyncTeX')) {
      e.preventDefault();
      this.pdfDocument.getPage(page).then((pdfPage) => {
        let viewport = pdfPage.getViewport(this.currentScale);
        [x,y] = viewport.convertToPdfPoint(e.offsetX, $(this.canvases[page - 1]).height() - e.offsetY);

        let callback = ((error, stdout, stderr) => {
          if (!error) {
            stdout = stdout.replace(/\r\n/g, '\n');
            let attrs = {};
            for (let line of stdout.split('\n')) {
              let m = line.match(/^([a-zA-Z]*):(.*)$/)
              if (m) {
                attrs[m[1]] = m[2];
              }
            }

            let file = attrs.Input;
            let line = attrs.Line;

            if (file && line) {
              let editor = null;
              let pathToOpen = path.normalize(attrs.Input);
              let lineToOpen = +attrs.Line;
              let done = false;
              for (let editor of atom.workspace.getTextEditors()) {
                if (editor.getPath() === pathToOpen) {
                  let position = new Point(lineToOpen-1, -1);
                  editor.scrollToBufferPosition(position, {center: true});
                  editor.setCursorBufferPosition(position);
                  editor.moveToFirstCharacterOfLine();
                  let pane = atom.workspace.paneForItem(editor);
                  pane.activateItem(editor);
                  pane.activate();
                  done = true;
                  break;
                }
              }

              if (!done) {
                atom.workspace.open(pathToOpen, {initialLine: lineToOpen, initialColumn: 0})
              }
            }
          }
        });

        let synctexPath = atom.config.get('pdf-view.syncTeXPath');
        let clickspec = [page, x, y, this.filePath].join(':');

        if (synctexPath) {
          execFile(synctexPath, ["edit", "-o", clickspec], callback);
        } else {
          let cmd = `synctex edit -o "${clickspec}"`;
          exec(cmd, callback);
        }
      });
    }
  }

  forwardSync(texPath, lineNumber) {
      let callback = ((error, stdout, stderr) => {
        if (!error) {
          stdout = stdout.replace(/\r\n/g, '\n');
          let attrs = {};
          for (let line of stdout.split('\n')) {
            let m = line.match(/^([a-zA-Z]*):(.*)$/)
            if (m) {
              attrs[m[1]] = m[2];
            }
          }

          let page = attrs.Page;
          this.scrollToPage(page)
        }
      });

      let synctexPath = atom.config.get('pdf-view.syncTeXPath');
      let inputspec = [lineNumber, 0, texPath].join(':');

      if (synctexPath) {
        execFile(synctexPath, ["view", "-i", inputspec, "-o", this.filePath], callback);
      } else {
        let cmd = `synctex view -i "${inputspec}" -o "${this.filePath}"`;
        exec(cmd, callback);
      }
  }

  onScroll() {
    if (!this.updating) {
      this.scrollTopBeforeUpdate = this.scrollTop();
      this.scrollLeftBeforeUpdate = this.scrollLeft();
    }

    this.setCurrentPageNumber();
  }

  setCurrentPageNumber() {
    if (!this.pdfDocument) {
      return;
    }

    let center = (this.scrollBottom() + this.scrollTop())/2.0
    this.currentPageNumber = 1

    if (this.centersBetweenPages.length === 0 && this.pageHeights.length === this.pdfDocument.numPages)
      for (let pdfPageNumber of _.range(1, this.pdfDocument.numPages+1)) {
        this.centersBetweenPages.push(this.pageHeights.slice(0, pdfPageNumber).reduce(((x,y) => x + y), 0) + pdfPageNumber * 20 - 10);
      }

    for (let pdfPageNumber of _.range(2, this.pdfDocument.numPages+1)) {
      if (center >= this.centersBetweenPages[pdfPageNumber-2] && center < this.centersBetweenPages[pdfPageNumber-1]) {
        this.currentPageNumber = pdfPageNumber;
      }
    }

    atom.views.getView(atom.workspace).dispatchEvent(new Event('pdf-view:current-page-update'));
  }

  finishUpdate() {
    this.updating = false;
    if (this.fileChanged) {
      this.updatePdf();
    }
  }

  updatePdf() {
    this.fileChanged = false;

    if (!fs.existsSync(this.filePath)) {
      return;
    }

    let pdfData = null;

    try {
      pdfData = new Uint8Array(fs.readFileSync(this.filePath));
    } catch (error) {
      if (error.code === 'ENOENT') {
        return;
      }
    }

    this.updating = true;
    this.container.find("canvas").remove();
    this.canvases = [];

    PDFJS.getDocument(pdfData).then((pdfDocument) => {
      this.pdfDocument = pdfDocument;
      this.totalPageNumber = this.pdfDocument.numPages;

      for (let pdfPageNumber of _.range(1, this.pdfDocument.numPages+1)) {
        let canvas = $("<canvas/>", {class: "page-container"}).appendTo(this.container)[0];
        this.canvases.push(canvas);
        $(canvas).on('click', (e) => this.onCanvasClick(pdfPageNumber, e));
      }

      this.renderPdf();
    }, () => this.finishUpdate());
  }

  renderPdf(scrollAfterRender = true) {
    this.centersBetweenPages = [];
    this.pageHeights = [];

    for (let pdfPageNumber of _.range(1, this.pdfDocument.numPages+1)) {
      let canvas = this.canvases[pdfPageNumber-1];

      this.pdfDocument.getPage(pdfPageNumber).then((pdfPage) => {
        let viewport = pdfPage.getViewport(this.currentScale);
        let context = canvas.getContext('2d');

        let outputScale = window.devicePixelRatio;
        canvas.height = Math.floor(viewport.height) * outputScale;
        canvas.width = Math.floor(viewport.width) * outputScale;

        if (outputScale != 1) {
          context._scaleX = outputScale;
          context._scaleY = outputScale;
          context.scale(outputScale, outputScale);
          canvas.style.width = Math.floor(viewport.width) + 'px';
          canvas.style.height = Math.floor(viewport.height) + 'px';
        }

        this.pageHeights.push(Math.floor(viewport.height));

        pdfPage.render({canvasContext: context, viewport: viewport});

        if (pdfPage.pageNumber === this.pdfDocument.numPages && scrollAfterRender) {
          this.scrollTop(this.scrollTopBeforeUpdate);
          this.scrollLeft(this.scrollLeftBeforeUpdate);
          this.setCurrentPageNumber();
          this.finishUpdate();
        }
      }, () => this.finishUpdate());
    }
  }

  zoomOut() {
    return this.adjustSize(100 / (100 + this.scaleFactor));
  }

  zoomIn() {
    return this.adjustSize((100 + this.scaleFactor) / 100);
  }

  resetZoom() {
    return this.adjustSize(this.defaultScale / this.currentScale);
  }

  computeZoomedScrollTop(oldScrollTop, oldPageHeights) {
    let pixelsToZoom = 0;
    let spacesToSkip = 0;
    let zoomedPixels = 0;

    for (let pdfPageNumber of _.range(0, this.pdfDocument.numPages)) {
      if (pixelsToZoom + spacesToSkip + oldPageHeights[pdfPageNumber] > oldScrollTop) {
        zoomFactorForPage = this.pageHeights[pdfPageNumber] / oldPageHeights[pdfPageNumber];
        let partOfPageAboveUpperBorder = oldScrollTop - (pixelsToZoom + spacesToSkip);
        zoomedPixels += Math.round(partOfPageAboveUpperBorder * zoomFactorForPage);
        pixelsToZoom += partOfPageAboveUpperBorder;
        break;
      } else {
        pixelsToZoom += oldPageHeights[pdfPageNumber];
        zoomedPixels += this.pageHeights[pdfPageNumber];
      }

      if (pixelsToZoom + spacesToSkip + 20 > oldScrollTop) {
        let partOfPaddingAboveUpperBorder = oldScrollTop - (pixelsToZoom + spacesToSkip);
        spacesToSkip += partOfPaddingAboveUpperBorder;
        break;
      } else {
        spacesToSkip += 20;
      }
    }

    return zoomedPixels + spacesToSkip;
  }

  adjustSize(factor) {
    if (!this.pdfDocument) {
      return;
    }

    let oldScrollTop = this.scrollTop();
    let oldPageHeights = this.pageHeights.slice(0);
    this.currentScale = this.currentScale * factor;
    this.renderPdf(false);

    process.nextTick(() => {
      let newScrollTop = this.computeZoomedScrollTop(oldScrollTop, oldPageHeights);
      this.scrollTop(newScrollTop);
    });

    process.nextTick(() => {
      let newScrollLeft = this.scrollLeft() * factor;
      this.scrollLeft(newScrollLeft);
    });
  }

  getCurrentPageNumber() {
    return this.currentPageNumber;
  }

  getTotalPageNumber() {
    return this.totalPageNumber;
  }

  scrollToPage(pdfPageNumber) {
    if (!this.pdfDocument || isNaN(pdfPageNumber)) {
      return;
    }

    pdfPageNumber = Math.min(pdfPageNumber, this.pdfDocument.numPages);
    pageScrollPosition = (this.pageHeights.slice(0, (pdfPageNumber-1)).reduce(((x,y) => x+y), 0)) + (pdfPageNumber - 1) * 20

    return this.scrollTop(pageScrollPosition);
  }

  serialize() {
    return {filePath: this.filePath, deserializer: 'PdfEditorDeserializer'};
  }

  getTitle() {
    if (this.filePath) {
      return path.basename(this.filePath);
    } else {
      return 'untitled';
    }
  }

  getURI() {
    return this.filePath;
  }

  getPath() {
    return this.filePath;
  }

  destroy() {
    return this.detach();
  }

  onDidChangeTitle() {
    return new Disposable(() => null);
  }

  onDidChangeModified() {
    return new Disposable(() => null);
  }
}
<|endoftext|>


<commit_before>/* global define */

define([
  'controllers/base-controller',
  'controllers/archive-controller',
  'views/search-view',
  'collections/post-collection',
  'helpers/post-filter',
  'buses/command-bus',
  'buses/navigator'
], function (BaseController, ArchiveController, SearchView, Posts, PostFilter, CommandBus, Navigator) {
  'use strict';

  var SearchController = BaseController.extend({
    busEvents: {
      'search:view:search:term':   'searchTerm',
      'search:view:search:submit': 'navigateSearchUrl',
      'search:view:search:empty':  'teardownSearch'
    },

    initialize: function (options) {
      options     = options || {};
      this.filter = options.filter || new PostFilter();
      this.posts  = options.posts || new Posts(null, { filter: this.filter });
      this._handleCommands();
    },

    showSearch: function (options) {
      this.show(new SearchView(), { region: options.region });
    },

    onBeforeDestroy: function () {
      this.archive.mainView.destroy();
    },

    searchTerm: function (options) {
      var page = options.paged || 1;

      this.filter.bySearchingFor(options.search).onPage(page);
      if (this.posts.length > 0) {
        this._archiveController().triggerMethod('search:term');
      } else {
        this._archiveController().showArchive();
      }
    },

    navigateSearchUrl: function (options) {
      var page = null, trigger = false;
      Navigator.navigateToSearch(options.search, page, trigger);
    },

    teardownSearch: function () {
      this.posts.reset();
      Navigator.navigateToCurrent();
    },

    _archiveController: function () {
      if (!this.archive) {
        this.archive = new ArchiveController({ posts: this.posts, filter: this.filter });
      }

      return this.archive;
    },

    _handleCommands: function () {
      CommandBus.setHandler('search:term', this.searchTerm, this);
    }
  });

  return SearchController;
});<commit_msg>Avoid destroying an unset view<commit_after>/* global define */

define([
  'controllers/base-controller',
  'controllers/archive-controller',
  'views/search-view',
  'collections/post-collection',
  'helpers/post-filter',
  'buses/command-bus',
  'buses/navigator'
], function (BaseController, ArchiveController, SearchView, Posts, PostFilter, CommandBus, Navigator) {
  'use strict';

  var SearchController = BaseController.extend({
    busEvents: {
      'search:view:search:term':   'searchTerm',
      'search:view:search:submit': 'navigateSearchUrl',
      'search:view:search:empty':  'teardownSearch'
    },

    initialize: function (options) {
      options     = options || {};
      this.filter = options.filter || new PostFilter();
      this.posts  = options.posts || new Posts(null, { filter: this.filter });
      this._handleCommands();
    },

    showSearch: function (options) {
      this.show(new SearchView(), { region: options.region });
    },

    onBeforeDestroy: function () {
      if (this.archive) {
        this.archive.mainView.destroy();
      }
    },

    searchTerm: function (options) {
      var page = options.paged || 1;

      this.filter.bySearchingFor(options.search).onPage(page);
      if (this.posts.length > 0) {
        this._archiveController().triggerMethod('search:term');
      } else {
        this._archiveController().showArchive();
      }
    },

    navigateSearchUrl: function (options) {
      var page = null, trigger = false;
      Navigator.navigateToSearch(options.search, page, trigger);
    },

    teardownSearch: function () {
      this.posts.reset();
      Navigator.navigateToCurrent();
    },

    _archiveController: function () {
      if (!this.archive) {
        this.archive = new ArchiveController({ posts: this.posts, filter: this.filter });
      }

      return this.archive;
    },

    _handleCommands: function () {
      CommandBus.setHandler('search:term', this.searchTerm, this);
    }
  });

  return SearchController;
});<|endoftext|>


<commit_before>/*!
 * Module dependencies.
 */

var cordova = require('cordova'),
    events = require('events'),
    shell = require('shelljs'),
    path = require('path');

/**
 * Create a New App.
 *
 * Creates an project on the local filesystem.
 * This project is backwards compatible with Apache Cordova projects.
 *
 * Options:
 *
 *   - `options` {Object} is data required to create an app
 *     - `path` {String} is a directory path for the app.
 *   - [`callback`] {Function} is triggered after creating the app.
 *     - `e` {Error} is null unless there is an error.
 *
 * Events:
 *
 *   - `error` is trigger on an error.
 *     - `e` {Error} details the error.
 *   - `complete` is trigger when no error occurs.
 */

module.exports = function(options, callback) {
    // require options
    if (!options) throw new Error('requires option parameter');
    if (!options.path) throw new Error('requires option.path parameter');

    // optional callback
    callback = callback || function() {};

    // expand path
    options.path = path.resolve(options.path);

    // event support
    var emitter = new events.EventEmitter();
    emitter.on('error', callback);
    emitter.on('complete', function() {
        callback(null);
    });

    // create app
    process.nextTick(function() {
        execute(options, emitter);
    });

    return emitter;
};

/**
 * Replace Cordova Project with PhoneGap Project.
 *
 * This function can be removed once cordova-cli supports external projects.
 *
 * Options:
 *
 *   - `options` {Object}
 *   - `options.path` {String} is the project path.
 */

module.exports.updateProject = function(options) {
    var destinationPath = path.join(options.path, 'www'),
        sourcePath = path.join(__dirname, '..', '..', 'node_modules', 'phonegap-build', 'res', 'project-template', 'www');

    shell.rm('-r', destinationPath);
    shell.cp('-r', sourcePath, options.path);
};

/*!
 * Execute.
 */

var execute = function(options, emitter) {
    // create local project
    try {
        cordova.create(options.path);

        // use phonegap hello world
        module.exports.updateProject(options);

        emitter.emit('complete');
    }
    catch(err) {
        // workaround because cordova-cli throws String and Error objects.
        var e = (err instanceof Error) ? err : new Error(err);
        emitter.emit('error', e);
    }
};
<commit_msg>[lib] Add note about cordova-cli error inconsistency patch.<commit_after>/*!
 * Module dependencies.
 */

var cordova = require('cordova'),
    events = require('events'),
    shell = require('shelljs'),
    path = require('path');

/**
 * Create a New App.
 *
 * Creates an project on the local filesystem.
 * This project is backwards compatible with Apache Cordova projects.
 *
 * Options:
 *
 *   - `options` {Object} is data required to create an app
 *     - `path` {String} is a directory path for the app.
 *   - [`callback`] {Function} is triggered after creating the app.
 *     - `e` {Error} is null unless there is an error.
 *
 * Events:
 *
 *   - `error` is trigger on an error.
 *     - `e` {Error} details the error.
 *   - `complete` is trigger when no error occurs.
 */

module.exports = function(options, callback) {
    // require options
    if (!options) throw new Error('requires option parameter');
    if (!options.path) throw new Error('requires option.path parameter');

    // optional callback
    callback = callback || function() {};

    // expand path
    options.path = path.resolve(options.path);

    // event support
    var emitter = new events.EventEmitter();
    emitter.on('error', callback);
    emitter.on('complete', function() {
        callback(null);
    });

    // create app
    process.nextTick(function() {
        execute(options, emitter);
    });

    return emitter;
};

/**
 * Replace Cordova Project with PhoneGap Project.
 *
 * This function can be removed once cordova-cli supports external projects.
 *
 * Options:
 *
 *   - `options` {Object}
 *   - `options.path` {String} is the project path.
 */

module.exports.updateProject = function(options) {
    var destinationPath = path.join(options.path, 'www'),
        sourcePath = path.join(__dirname, '..', '..', 'node_modules', 'phonegap-build', 'res', 'project-template', 'www');

    shell.rm('-r', destinationPath);
    shell.cp('-r', sourcePath, options.path);
};

/*!
 * Execute.
 */

var execute = function(options, emitter) {
    // create local project
    try {
        cordova.create(options.path);

        // use phonegap hello world
        module.exports.updateProject(options);

        emitter.emit('complete');
    }
    catch(err) {
        // workaround because cordova-cli throws String and Error objects.
        // CB-2733 has fixed the inconsistencies. This will be live on the
        // next published release of cordova-cli.
        var e = (err instanceof Error) ? err : new Error(err);
        emitter.emit('error', e);
    }
};
<|endoftext|>


<commit_before>import Ember from 'ember';

export default Ember.Service.extend({
  store: Ember.inject.service(),
  routing: Ember.inject.service('-routing'),
  feedModel: null,
  userModel: null,
  parseFetchInfoResponse: function(response) {
    var store = this.get('store');
    var existing = response.warnings.map(function(i) {return i.onestop_id});
    // Feeds
    var feed_data = store.normalize('feed', response.feed);
    var feedModel = existing.contains(response.feed.onestop_id) ? store.push('feed', feed_data) : store.createRecord('feed', feed_data);
    // Operators
    (response.operators || []).forEach(function(operator) {
      operator.represented_in_feed_onestop_ids = [feedModel.id];
      var operator_data = store.normalize('operator', operator);
      var operatorModel = existing.contains(operator.onestop_id) ? store.push('operator', operator_data) : store.createRecord('operator', operator_data);
    });
    // Refresh the feedModel reference;
    // feedModel = store.peekRecord('feed', feed_onestop_id);
    this.set('feedModel', feedModel);
    return feedModel;
  },
  parseFetchInfoErrors: function(response){
    var feedModel = this.get('feedModel');
    response.errors.forEach(function(error){
      feedModel.get('errors').add('url', error.message);
    });
  },
  createFeedAndUserModels: function() {
    var newFeedModel = this.get('store').createRecord('feed', {});
		var newUserModel = this.get('store').createRecord('user', {});
    this.set('feedModel', newFeedModel);
    this.set('userModel', newUserModel);
    return {
      feed: newFeedModel,
      user: newUserModel
    };
  },
  getFeedOrTransitionToStart: function() {
    if (this.feedModel === null) {
      return this.get('routing').transitionTo('feeds.new');
    } else {
      return this.feedModel;
    }
  },
  toChangeset: function() {
    var feedModel = this.get('feedModel');
    var changes = [];
    feedModel.get('operators').map(function(operator) {
      if (operator.get('include_in_changeset') === true) {
        changes.push({
          action: 'createUpdate',
          operator: operator.toChange()
        });
      }
    });
    changes.push({
      action: 'createUpdate',
      feed: feedModel.toChange()
    });
    var changeset = this.get('store').createRecord('changeset', {
      notes: `Changed submitted from Feed Registry for feed at ${feedModel.get('url')}`,
      payload: {
        changes: changes
      }
    });
		changeset.set('user', this.get('userModel'));
    return changeset;
  }
});
<commit_msg>Fix issue w/ normalize<commit_after>import Ember from 'ember';

export default Ember.Service.extend({
  store: Ember.inject.service(),
  routing: Ember.inject.service('-routing'),
  feedModel: null,
  userModel: null,
  parseFetchInfoResponse: function(response) {
    var store = this.get('store');
    var existing = response.warnings.map(function(i) {return i.onestop_id});
    // Feeds
    // Note: store.normalize operates in-place!
    var feed_data = store.normalize('feed', response.feed);
    var feedModel = existing.contains(feed_data.id) ? store.push('feed', feed_data) : store.createRecord('feed', feed_data);
    console.log("Feed:", feedModel.id, " state:", feedModel.get('currentState.stateName'));
    // Operators
    store.unloadAll('operator');
    (response.operators || []).forEach(function(operator) {
      var operator_data = store.normalize('operator', operator);
      operator_data.feeds = [feedModel];
      var operatorModel = existing.contains(operator_data.id) ? store.push('operator', operator_data) : store.createRecord('operator', operator_data);
      console.log("Operator:", operatorModel.id, " state:", operatorModel.get('currentState.stateName'));
    });
    // Reset the feedModel reference;
    this.set('feedModel', feedModel);
    return feedModel;
  },
  parseFetchInfoErrors: function(response){
    var feedModel = this.get('feedModel');
    response.errors.forEach(function(error){
      feedModel.get('errors').add('url', error.message);
    });
  },
  createFeedAndUserModels: function() {
    var newFeedModel = this.get('store').createRecord('feed', {});
		var newUserModel = this.get('store').createRecord('user', {});
    this.set('feedModel', newFeedModel);
    this.set('userModel', newUserModel);
    return {
      feed: newFeedModel,
      user: newUserModel
    };
  },
  getFeedOrTransitionToStart: function() {
    if (this.feedModel === null) {
      return this.get('routing').transitionTo('feeds.new');
    } else {
      return this.feedModel;
    }
  },
  toChangeset: function() {
    var feedModel = this.get('feedModel');
    var changes = [];
    feedModel.get('operators').map(function(operator) {
      if (operator.get('include_in_changeset') === true) {
        changes.push({
          action: 'createUpdate',
          operator: operator.toChange()
        });
      }
    });
    changes.push({
      action: 'createUpdate',
      feed: feedModel.toChange()
    });
    var changeset = this.get('store').createRecord('changeset', {
      notes: `Changed submitted from Feed Registry for feed at ${feedModel.get('url')}`,
      payload: {
        changes: changes
      }
    });
		changeset.set('user', this.get('userModel'));
    return changeset;
  }
});
<|endoftext|>


<commit_before>// PUBLIC FUNCTIONS

// http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
vistk.utils.merge = function(d, e) {

  var obj = {},
      i = 0,
      il = arguments.length,
      key;
  for (; i < il; i++) {
      for (key in arguments[i]) {
          if (arguments[i].hasOwnProperty(key)) {
              obj[key] = arguments[i][key];
          }
      }
  }
  return obj;
};

// TODO: add accessor as argument and var_time
vistk.utils.flatten_years = function(data) {
  var flat = [];

  //for each country
  data.forEach(function(root) {

      //for each year in each country
      root.years.forEach(function(year) {
          //extend the year object with the common properties stored just once in the country object

        var current_year = vistk.utils.merge(root, year);
        delete current_year.years;

          //add it to the final flat array
          flat.push(current_year);
      })
  });

  return flat;
}

vistk.utils.find_node_by_id = function(nodes, id) {
  var res = nodes.filter(function(d) {
    return d.id == id;
  })[0];

  return res;
}

vistk.utils.find_node_coordinates_by_id = function(nodes, var_id, data_id) {

  var res = nodes.filter(function(node) {
    return node[var_id] == data_id;
  })[0];

  return res;
}

vistk.utils.max = function(data) {
  var var_time = this.var_time;
  return d3.max(data, function(d) { return d[var_time] });
}

vistk.utils.min = function(data) {
  var var_time = this.var_time;
  return d3.min(data, function(d) { return d[var_time] });
}

vistk.utils.extent = function(data, var_data, time) {
  if(typeof time === 'undefined') {
    return d3.extent(data, function(d) { return d[var_data]; });
  } else {
    return d3.extent(data, function(d) {
      return d.values[time][var_data];
    });
  }

}

vistk.utils.time = {};

vistk.utils.time.current = function(data) {
  return vars.current_time;
}

vistk.utils.decode_url = function() {

  // Variables from query string
  var queryParameters = {}, queryString = location.search.substring(1),
      re = /([^&=]+)=([^&]*)/g, m, queryActivated = false;

  // Creates a map with the query string parameters
  while (m = re.exec(queryString)) {
    queryParameters[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
  }

  return queryParameters;
}

// Credits: http://bl.ocks.org/mbostock/1705868
vistk.utils.translate_along = function(path) {
  var l = path.node().getTotalLength();
  return function(d, i, a) {
    return function(t) {
      var p = path.node().getPointAtLength(t * l);
      return "translate(" + p.x + "," + p.y + ")";
    };
  };
}

vistk.utils.find_unique_values = function(data, key) {

  var unique = {};
  var result = [];

  for(var i in data){
    if(typeof(unique[data[i][key]]) == "undefined"){
      result.push(data[i][key]);
    }
    unique[data[i][key]] = 0;
  }

  return result;

}

vistk.utils.list = function(param) {
  return list[param];
}

vistk.utils.scale = {};

vistk.utils.scale.linear = function(vars) {

  return [{
    func: d3.scale.linear()
            .range([vars.margin.left, vars.width - vars.margin.left - vars.margin.right])
            .domain([vars.margin.left, vars.width - vars.margin.left - vars.margin.right])
  }];

};

vistk.utils.scale.none = function() {

  return [{
    func: d3.scale.linear()
            .range([0, 0])
  }];

};

vistk.utils.scale.x = {};

vistk.utils.scale.x.center = function() {

  return [{
    func: d3.scale.linear()
            .range([0, 0])
  }];

};

vistk.utils.init_item = function(d) {
  d.__aggregated = false;
  d.__selected = false;
  d.__selected__adjacent = false;
  d.__highlighted = false;
  d.__highlighted__adjacent = false;
  d.__missing = false;
  d.__redraw = false;
};

// Creates
vistk.utils.aggregate = function(data, vars, var_agg, type_agg) {

  var type_agg = type_agg || 'sum';

  return d3.nest()
        .key(function(d) {

          if(vars.time.current_time === null) {
            return d[var_agg];
          } else {
            return d.values[vars.time.current_time][var_agg];
          }

        })
        .rollup(function(leaves) {

          // Generates a new dataset with aggregated data
          var aggregation = {};

          if(vars.time.current_time === null) {
            aggregation[vars.var_id] = 'agg_' + leaves[0][var_agg];
          } else {
            aggregation[vars.var_id] = 'agg_' + leaves[0].values[vars.time.current_time][var_agg] + Math.random();
          }

          // Name and id values are

          aggregation[vars.var_text] = leaves[0][vars.var_group];
          aggregation[vars.var_group] = leaves[0][vars.var_group];

          // Quick fix in case var_x is an ordinal scale
          if(vars.var_x !== vars.var_id && vars.var_x !== vars.time.var_time && vars.var_x !== vars.var_group) {

            aggregation[vars.var_x] = d3.mean(leaves, function(d) {
              return d[vars.var_x];
            });

          } else {

            aggregation[vars.var_x] = leaves[0][vars.var_x];

          }

          if(vars.var_y !== vars.var_id && vars.var_y !== vars.time.var_time) {

            aggregation[vars.var_y] = d3.mean(leaves, function(d) {
              return d[vars.var_y];
            });

          } else {

            aggregation[vars.var_y] = leaves[0][vars.var_y];

          }

          aggregation[vars.var_r] = d3.sum(leaves, function(d) {
            return d[vars.var_r];
          });

          aggregation[vars.var_share] = d3.sum(leaves, function(d) {
            return d[vars.var_share];
          });

          aggregation[vars.var_color] = leaves[0][vars.var_color];

          // Init temporal values
          aggregation.values = [];

          // Assuming all the time values are present in all items
          vars.time.points.forEach(function(time, i) {

            var d = {};

           // if(vars.var_x === vars.time.var_time) {
           //   d[vars.var_x] = leaves[0].values[i][vars.var_x];
           // } else {
           //   d[vars.var_x] = 0;
           // }

            // Init values
            d[vars.var_x] = leaves[0].values[time][vars.var_x];
            d[vars.var_y] = leaves[0].values[time][vars.var_y];
            d[vars.var_r] = leaves[0].values[time][vars.var_r];

            d[vars.var_share] = leaves[0].values[time][vars.var_share];
            d[vars.var_color] = leaves[0].values[time][vars.var_color];
            d[vars.var_size] = leaves[0].values[time][vars.var_size];
            d[vars.var_text] = leaves[0].values[time][vars.var_text];

            // Time var
            d[vars.time.var_time] = leaves[0].values[time][vars.time.var_time];

            // Should stay at the bottom to make sure it's not overriden
            d[vars.var_id] = 'agg_' + leaves[0].values[time][vars.var_id];

            d.__index = 'agg_' + leaves[0].values[time].__index;

            aggregation.values[time] = d;

            //return d;
          });

          // Assuming we only aggregate var_x, var_y, var_r
          leaves.forEach(function(d, i) {

            vars.time.points.forEach(function(time, i) {

              if(vars.var_x !== vars.time.var_time) {
                aggregation.values[time][vars.var_x] += d.values[time][vars.var_x];
              }

              if(vars.var_y !== vars.time.var_time) {
                aggregation.values[time][vars.var_y] += d.values[time][vars.var_y];
              }

              aggregation.values[time][vars.var_r] += d.values[time][vars.var_r];

            });

          });

          vistk.utils.init_item(aggregation);

          // Set internal attributes specific to aggregate + force redraw
          aggregation.__aggregated = true;
          aggregation.__redraw = true;

          vars.columns.forEach(function(c) {

            // Do no aggreagete ordinal values
            if(c === vars.var_text || c === vars.var_group) {
              return;
            }

            aggregation[c] = d3.mean(leaves, function(d) {
              return d[c];
            });

          });

          return aggregation;
        })
        .entries(data);

}

vistk.utils.colors = {};

vistk.utils.colors.products_hs4 = d3.scale.ordinal().domain([0, 9]).range(["#3182bd", "#6baed6", "#9ecae1", "#c6dbef", "#e6550d", "#fd8d3c", "#fdae6b", "#fdd0a2", "#31a354", "#74c476", "#a1d99b", "#c7e9c0", "#756bb1", "#9e9ac8", "#bcbddc", "#dadaeb", "#636363", "#969696", "#bdbdbd", "#d9d9d9"])
<commit_msg>Adding default color scales for world countries<commit_after>// PUBLIC FUNCTIONS

// http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
vistk.utils.merge = function(d, e) {

  var obj = {},
      i = 0,
      il = arguments.length,
      key;
  for (; i < il; i++) {
      for (key in arguments[i]) {
          if (arguments[i].hasOwnProperty(key)) {
              obj[key] = arguments[i][key];
          }
      }
  }
  return obj;
};

// TODO: add accessor as argument and var_time
vistk.utils.flatten_years = function(data) {
  var flat = [];

  //for each country
  data.forEach(function(root) {

      //for each year in each country
      root.years.forEach(function(year) {
          //extend the year object with the common properties stored just once in the country object

        var current_year = vistk.utils.merge(root, year);
        delete current_year.years;

          //add it to the final flat array
          flat.push(current_year);
      })
  });

  return flat;
}

vistk.utils.find_node_by_id = function(nodes, id) {
  var res = nodes.filter(function(d) {
    return d.id == id;
  })[0];

  return res;
}

vistk.utils.find_node_coordinates_by_id = function(nodes, var_id, data_id) {

  var res = nodes.filter(function(node) {
    return node[var_id] == data_id;
  })[0];

  return res;
}

vistk.utils.max = function(data) {
  var var_time = this.var_time;
  return d3.max(data, function(d) { return d[var_time] });
}

vistk.utils.min = function(data) {
  var var_time = this.var_time;
  return d3.min(data, function(d) { return d[var_time] });
}

vistk.utils.extent = function(data, var_data, time) {
  if(typeof time === 'undefined') {
    return d3.extent(data, function(d) { return d[var_data]; });
  } else {
    return d3.extent(data, function(d) {
      return d.values[time][var_data];
    });
  }

}

vistk.utils.time = {};

vistk.utils.time.current = function(data) {
  return vars.current_time;
}

vistk.utils.decode_url = function() {

  // Variables from query string
  var queryParameters = {}, queryString = location.search.substring(1),
      re = /([^&=]+)=([^&]*)/g, m, queryActivated = false;

  // Creates a map with the query string parameters
  while (m = re.exec(queryString)) {
    queryParameters[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
  }

  return queryParameters;
}

// Credits: http://bl.ocks.org/mbostock/1705868
vistk.utils.translate_along = function(path) {
  var l = path.node().getTotalLength();
  return function(d, i, a) {
    return function(t) {
      var p = path.node().getPointAtLength(t * l);
      return "translate(" + p.x + "," + p.y + ")";
    };
  };
}

vistk.utils.find_unique_values = function(data, key) {

  var unique = {};
  var result = [];

  for(var i in data){
    if(typeof(unique[data[i][key]]) == "undefined"){
      result.push(data[i][key]);
    }
    unique[data[i][key]] = 0;
  }

  return result;

}

vistk.utils.list = function(param) {
  return list[param];
}

vistk.utils.scale = {};

vistk.utils.scale.linear = function(vars) {

  return [{
    func: d3.scale.linear()
            .range([vars.margin.left, vars.width - vars.margin.left - vars.margin.right])
            .domain([vars.margin.left, vars.width - vars.margin.left - vars.margin.right])
  }];

};

vistk.utils.scale.none = function() {

  return [{
    func: d3.scale.linear()
            .range([0, 0])
  }];

};

vistk.utils.scale.x = {};

vistk.utils.scale.x.center = function() {

  return [{
    func: d3.scale.linear()
            .range([0, 0])
  }];

};

vistk.utils.init_item = function(d) {
  d.__aggregated = false;
  d.__selected = false;
  d.__selected__adjacent = false;
  d.__highlighted = false;
  d.__highlighted__adjacent = false;
  d.__missing = false;
  d.__redraw = false;
};

// Creates
vistk.utils.aggregate = function(data, vars, var_agg, type_agg) {

  var type_agg = type_agg || 'sum';

  return d3.nest()
        .key(function(d) {

          if(vars.time.current_time === null) {
            return d[var_agg];
          } else {
            return d.values[vars.time.current_time][var_agg];
          }

        })
        .rollup(function(leaves) {

          // Generates a new dataset with aggregated data
          var aggregation = {};

          if(vars.time.current_time === null) {
            aggregation[vars.var_id] = 'agg_' + leaves[0][var_agg];
          } else {
            aggregation[vars.var_id] = 'agg_' + leaves[0].values[vars.time.current_time][var_agg] + Math.random();
          }

          // Name and id values are

          aggregation[vars.var_text] = leaves[0][vars.var_group];
          aggregation[vars.var_group] = leaves[0][vars.var_group];

          // Quick fix in case var_x is an ordinal scale
          if(vars.var_x !== vars.var_id && vars.var_x !== vars.time.var_time && vars.var_x !== vars.var_group) {

            aggregation[vars.var_x] = d3.mean(leaves, function(d) {
              return d[vars.var_x];
            });

          } else {

            aggregation[vars.var_x] = leaves[0][vars.var_x];

          }

          if(vars.var_y !== vars.var_id && vars.var_y !== vars.time.var_time) {

            aggregation[vars.var_y] = d3.mean(leaves, function(d) {
              return d[vars.var_y];
            });

          } else {

            aggregation[vars.var_y] = leaves[0][vars.var_y];

          }

          aggregation[vars.var_r] = d3.sum(leaves, function(d) {
            return d[vars.var_r];
          });

          aggregation[vars.var_share] = d3.sum(leaves, function(d) {
            return d[vars.var_share];
          });

          aggregation[vars.var_color] = leaves[0][vars.var_color];

          // Init temporal values
          aggregation.values = [];

          // Assuming all the time values are present in all items
          vars.time.points.forEach(function(time, i) {

            var d = {};

           // if(vars.var_x === vars.time.var_time) {
           //   d[vars.var_x] = leaves[0].values[i][vars.var_x];
           // } else {
           //   d[vars.var_x] = 0;
           // }

            // Init values
            d[vars.var_x] = leaves[0].values[time][vars.var_x];
            d[vars.var_y] = leaves[0].values[time][vars.var_y];
            d[vars.var_r] = leaves[0].values[time][vars.var_r];

            d[vars.var_share] = leaves[0].values[time][vars.var_share];
            d[vars.var_color] = leaves[0].values[time][vars.var_color];
            d[vars.var_size] = leaves[0].values[time][vars.var_size];
            d[vars.var_text] = leaves[0].values[time][vars.var_text];

            // Time var
            d[vars.time.var_time] = leaves[0].values[time][vars.time.var_time];

            // Should stay at the bottom to make sure it's not overriden
            d[vars.var_id] = 'agg_' + leaves[0].values[time][vars.var_id];

            d.__index = 'agg_' + leaves[0].values[time].__index;

            aggregation.values[time] = d;

            //return d;
          });

          // Assuming we only aggregate var_x, var_y, var_r
          leaves.forEach(function(d, i) {

            vars.time.points.forEach(function(time, i) {

              if(vars.var_x !== vars.time.var_time) {
                aggregation.values[time][vars.var_x] += d.values[time][vars.var_x];
              }

              if(vars.var_y !== vars.time.var_time) {
                aggregation.values[time][vars.var_y] += d.values[time][vars.var_y];
              }

              aggregation.values[time][vars.var_r] += d.values[time][vars.var_r];

            });

          });

          vistk.utils.init_item(aggregation);

          // Set internal attributes specific to aggregate + force redraw
          aggregation.__aggregated = true;
          aggregation.__redraw = true;

          vars.columns.forEach(function(c) {

            // Do no aggreagete ordinal values
            if(c === vars.var_text || c === vars.var_group) {
              return;
            }

            aggregation[c] = d3.mean(leaves, function(d) {
              return d[c];
            });

          });

          return aggregation;
        })
        .entries(data);

}

vistk.utils.colors = {};

vistk.utils.colors.products_hs4 = d3.scale.ordinal().domain([0, 9]).range(["#3182bd", "#6baed6", "#9ecae1", "#c6dbef", "#e6550d", "#fd8d3c", "#fdae6b", "#fdd0a2", "#31a354", "#74c476", "#a1d99b", "#c7e9c0", "#756bb1", "#9e9ac8", "#bcbddc", "#dadaeb", "#636363", "#969696", "#bdbdbd", "#d9d9d9"]);
vistk.utils.colors.world_regions = d3.scale.ordinal().domain(["Africa", "Americas", "Asia", "Europe", "Oceania"]).range(["#99237d", "#c72439", "#6bc145", "#88c7ed", "#dd9f98"]);
<|endoftext|>


<commit_before>import path from 'path';
const env = process.env.NODE_ENV || 'development';
let returnValue;

if (__CLIENT__) {
  if (env === 'production') {
    returnValue = require('env-production');
  } else {
    returnValue = require('env-development');
  }
} else {
  returnValue = require(path.resolve('envConfig', `env-${env}`));
}

export default returnValue;
<commit_msg>Move to environment variables<commit_after>// import path from 'path';
// const env = process.env.NODE_ENV || 'development';
// let returnValue;
//
// if (__CLIENT__) {
//  if (env === 'production') {
//    returnValue = require('env-production');
//  } else {
//    returnValue = require('env-development');
//  }
// } else {
//  returnValue = require(path.resolve('envConfig', `env-${env}`));
// }

export default {
  CONTENTFUL_SPACE: process.env.CONTENTFUL_SPACE,
  CONTENTFUL_ACCESS_TOKEN: process.env.CONTENTFUL_ACCESS_TOKEN
};
<|endoftext|>