private void applyStart(GridJob job) {

    String fullJobId = job.getFullJobId();
    Collection<JobActor> actors = jobActorMap.get(fullJobId);
    if (actors.isEmpty()) {
      JobActor jobActor = createJobActor(job);
      addJobActor(fullJobId, jobActor);
      actors = jobActorMap.get(fullJobId);
    }

    if (job.getNode() == null) {
      log.warn("No node for job being started: {}", fullJobId);
      return;
    }

    log.debug("Starting job {} on {}", fullJobId, job.getNode().getShortName());

    String nodeName = job.getNode().getShortName();
    if (actors.size() > 1) {
      log.warn("More than one actor for job being started: " + fullJobId);
    }

    JobActor jobActor = actors.iterator().next();

    int i = 0;
    GridJob[] nodeJobs = job.getNode().getSlots();
    for (int s = 0; s < nodeJobs.length; s++) {
      GridJob nodeJob = nodeJobs[s];
      if (nodeJob == null) continue;
      if (!nodeJob.getFullJobId().equals(fullJobId)) continue;

      if (i > 0) {
        jobActor = cloneJobActor(fullJobId);
      }

      PVector endPos = getLatticePos(nodeName, s + "");
      // TODO: random outside location in direction of vector from 0,0,0
      PVector startPos = new PVector(1000, 1000, 1000);
      jobActor.pos = startPos;

      if (tweenChanges) {
        // scale duration to the distance that needs to be traveled
        float distance = jobActor.pos.dist(endPos);
        float duration = (DURATION_JOB_START * distance / DISTANCE_JOB_START) * 0.6f;

        Tween tween =
            new Tween("start_job_" + fullJobId + "#" + i, getTweenDuration(duration))
                .addPVector(jobActor.pos, endPos)
                .call(jobActor, "jobStarted")
                .setEasing(Tween.SINE_OUT)
                .noAutoUpdate();

        jobActor.tweens.add(tween);
      } else {
        jobActor.pos.set(endPos);
      }

      i++;
    }
  }
  private JobActor cloneJobActor(String fullJobId) {
    Collection<JobActor> actors = jobActorMap.get(fullJobId);
    if (actors == null || actors.isEmpty()) {
      log.error("Cannot expand actors for non-existing job: " + fullJobId);
      return null;
    }

    JobActor jobActor = actors.iterator().next(); // first actor
    GridJob job = state.getJobByFullId(fullJobId);

    if (job == null) {
      log.error("Cannot expand actors for unknown job: " + fullJobId);
      return null;
    }

    JobActor copy = jobActor.copy();
    copy.name += "#" + actors.size();
    jobActorMap.put(fullJobId, copy);
    return copy;
  }
  private void applyEnd(GridJob job, long endOffset) {

    String fullJobId = job.getFullJobId();
    Collection<JobActor> actors = jobActorMap.get(fullJobId);
    if (actors == null || actors.isEmpty()) {
      log.warn("Cannot end job that does not exist: {}", fullJobId);
      return;
    }

    log.debug("Finishing job {} on node {}", fullJobId, job.getNode().getShortName());

    int i = 0;
    for (JobActor jobActor : actors) {

      PVector endPos = jobActor.getPos().get();
      endPos.mult(10.0f);

      if (tweenChanges) {
        // scale duration to the distance that needs to be traveled
        float distance = jobActor.pos.dist(endPos);
        float duration = (DURATION_JOB_END * distance / DURATION_JOB_END) * 0.6f;

        Tween tween =
            new Tween("end_job_" + fullJobId + "#" + i, getTweenDuration(duration))
                .addPVector(jobActor.pos, endPos)
                .call(jobActor, "jobEnded")
                .setEasing(Tween.SINE_IN)
                .noAutoUpdate();
        jobActor.tweens.add(tween);
      } else {
        jobActor.jobEnded();
      }

      i++;
    }
  }