차량 구현

2020년 12월 18일
Guidance Simulator

길다면 길고, 짧다면 짧은 시간 동안에 세 번째 글을 작성하게 되었네요. 아마 다음 글을 끝으로 이번 작업은 마무리 지을 수 있을 것 같다는 생각이 듭니다. 이번 글에서는 시뮬레이터에서 돌아다닐 차량에 대한 구현에 관해 이야기해보고자 합니다.

작동 방식

지난 글에서 차량의 구현에서 사용할 Worker에 대한 설정을 진행해두었기 때문에 차량은 그저 동작을 수행하고 이걸 Main으로 알려주는 방향으로 구상을 하였습니다.

export default class Vehicle extends EventEmitter {
  //...

  private work(): void {
    if (
      this.timer ||
      this.destroyed
    ) {
      return;
    }

    this.timer = self.setTimeout(() => {
      this.timer = null;
      const now = Date.now();
      const speed = this.velocity * this.acceleration;
      const time = (now - this.lastTime) / 1000 / 3600 * TIME_ACCELERATION; // second / hour * acceleration
      this.travelled += speed * time;
      this.lastTime = now;

      const { point, heading } = Vehicle.along(this.route, this.travelled);

      if (booleanEqual(point, this.last)) {
        this.setStatus(VehicleStatus.DONE);
        this.timer = self.setTimeout(() => this.initialize(), VEHICLE_NEXT_PERIOD);
        return;
      }

      const [lng, lat] = point.geometry.coordinates;
      this.moving({ lng, lat }, heading);
      this.work();
    }, 16.67);
  }

  //...
}

지난 글에서 작성한 TaskVehicle에서 Task는 거의 변경이 없었고, Vehicle은 차량이 일정 주기마다 경로를 따라 돌아다닐 수 있도록 기능인 work()를 추가하였습니다. work()의 기본적인 동작은 이전 수행 시간과 지금 시간의 차이와 정해진 속도로 이동할 거리를 환산하며, 이를 바탕으로 이전 이동 거리를 더해 along()을 호출하여 다음 위치를 찾게 됩니다. 이 과정에서 차량이 어느 방향을 보고 있는지도 가져오게 됩니다. 모든 작업이 끝나면 일정 시간이 지난 후에 initialize()를 호출하여 다음 작업을 수행하게 됩니다.

export default class Vehicle extends EventEmitter {
  //...

  private static along(
    line: Feature<LineString> | LineString,
    distance: number,
    options: { units?: Units } = {}
  ): { heading: number, point: Feature<Point> } {
    // Get Coords
    const geom = getGeom(line);
    const coords = geom.coordinates;
    let travelled = 0;
    for (let i = 0; i < coords.length; i++) {
      if (distance >= travelled && i === coords.length - 1) {
        break;
      } else if (travelled >= distance) {
        const overshot = distance - travelled;
        const direction = bearing(coords[i], coords[i - 1]) - 180;
        if (!overshot) {
          return {
            heading: direction,
            point: point(coords[i]),
          };
        } else {
          return {
            heading: direction,
            point: destination(
              coords[i],
              overshot,
              direction,
              options
            ),
          };
        }
      } else {
        travelled += measureDistance(coords[i], coords[i + 1], options);
      }
    }
    return {
      heading: bearing(coords[coords.length - 2], coords[coords.length - 1]),
      point: point(coords[coords.length - 1]),
    };
  }

  //...
}

차량에서 사용하는 along()@turf/along의 핵심 로직은 그대로 사용하고 있으며, 차량의 방향을 가져오기 위해서 heading에 대한 처리만 추가한 상태입니다. 당초에는 매번 처음 위치부터 다시 계산하는 것이 마음에 들지 않아 커스터 마이징하였으나 코드가 점점 복잡해지고 일부 구간에서 잘 동작하지 않는 것을 보고는 눈물을 머금고 전부 날려버렸습니다.

const speed = this.velocity * this.acceleration;
const time = (now - this.lastTime) / 1000 / 3600 * TIME_ACCELERATION; // second / hour * acceleration
this.travelled += speed * time;

이동 거리를 산정하는 기준에 대해서 조금 더 이야기해보자면 상단과 같습니다.

우선 차량 속도는 기본 차량 속력(velocity, 기본 60)에 가속 배율(acceleration, 기본 1)을 곱해서 가져오는 방향으로 생각했습니다. 추후 Simulator를 통해서 이 가속 배율을 조정하고자 했습니다.

차량 시간은 이전 수행 시간(lastTime)에서 현재 시간(now)의 차이를 구한 다음에 millisecond를 second로 변환하기 위해 1000을 나눠주었습니다. 이후 시속 60km를 기본 속도로 생각하였기 때문에 1시간을 초로 환산하여 3600을 추가로 나누어 주었습니다. 그렇게 되면 실제 시속 60km 속력으로 이동하게 되며, 이에 대해 보정을 하고자 시간에 대한 배율(TIME_ACCELERATION)을 곱해주었습니다. 저는 이 값을 분속 60km로 이동하는 것을 의도하고자 60으로 설정했습니다.

이후 속력과 시간을 곱해서 차량의 이동 거리를 구해 along()을 사용하여 다음 이동 위치를 구했습니다.

차량 노출

export interface VehicleMoving {
  id: string;
  position: LngLat;
  heading: number;
}
vehicle.on('moving', moving => {
  if (vehicle.getStatus() !== VehicleStatus.UNKNOWN) {
    movingSubject.next(moving);
  }
});

차량을 지도에 도출하기 위해 차량의 이동 상태를 담은 VehicleMoving이라는 값을 이벤트로 Main에 전달하였습니다. 전달받은 차량 이동 데이터를 바탕으로 ScenegraphLayer를 사용하여 Deck.gl에 노출하였습니다.

마치며

이번 작업에서 가장 필요하다고 생각했던 차량 이동에 대해 작업을 하고 작성한 글이라 감회가 새롭습니다. 이 글을 작성하는 시점에도 along()을 그대로 사용하는 부분이 불필요한 행동을 포함한다고 생각되어 아쉽지만, 성능에 문제가 발생하는 상황도 아님에도 불구하고 튜닝을 위해 너무 많은 시간을 사용하는 것 또한 불필요하다고 생각해서 아쉬움을 남겨두고 나아가고자 합니다. 다음 글은 여러 대의 차량을 노출할 수 있도록 Simulator를 구성하고 코드를 정리하고 마무리 짓는 글로 찾아뵙겠습니다.

Recently posts
© 2016-2023 smilecat.dev