4. 모빌리티 운영을 위한 디지털 트윈 시뮬레이션 구축#

4.1 간단한 예제로 알아보는 모빌리티 디지털 트윈 시뮬레이션 구현#

  • 이 절의 목표: 간단한 예제를 통해 데이터 생성→경로 계산→시간 스탬프 생성→저장→시각화까지 흐름을 빠르게 체험합니다.

  • 자세한 구성요소는 이후 절에서 단계별로 설명하며, 여기서는 전체 파이프라인을 한 번 훑습니다.

4.1.1 가상의 통행데이터 생성#

import numpy as np
import itertools
import requests
import polyline
import json
import os

import random as rd
import pandas as pd

from shapely.geometry import Point

from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry

import warnings 

warnings.filterwarnings('ignore')
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
Cell In[1], line 1
----> 1 import numpy as np
      2 import itertools
      3 import requests

ModuleNotFoundError: No module named 'numpy'

[1] 필수 함수 정의#

# 직선 거리 계산 함수
def calculate_straight_distance(lat1, lon1, lat2, lon2):
    # 지구 반경 (킬로미터 단위)
    km_constant = 3959* 1.609344
    # 위도와 경도를 라디안으로 변환
    lat1, lon1, lat2, lon2 = map(np.deg2rad, [lat1, lon1, lat2, lon2])
    # 위도 및 경도 차이 계산
    dlat = lat2 - lat1 
    dlon = lon2 - lon1
    # Haversine 공식 계산
    a = np.sin(dlat/2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon/2)**2
    c = 2 * np.arcsin(np.sqrt(a)) 
    # 거리 계산 (킬로미터 단위)
    km = km_constant * c
    
    return km
# osrm 기반의 Route 생성 함수
# 입력으로 받은 출발지와 목적지 좌표를 이용하여 경로 정보를 가져오는 함수
def get_res(point):

   status = 'defined'

   # 요청을 재시도할 수 있도록 세션 객체 생성 및 설정
   session = requests.Session()
   retry = Retry(connect=3, backoff_factor=0.5)
   adapter = HTTPAdapter(max_retries=retry)
   session.mount('http://', adapter)
   session.mount('https://', adapter)

   #### url 생성 코드
   # 전체 경로 정보를 요청
   overview = '?overview=full'
   # lon, lat, lon, lat 형식의 출발지 목적지 좌표
   loc = f"{point[0]},{point[1]};{point[2]},{point[3]}"
   # 보행경로 url
   url = 'http://router.project-osrm.org/route/v1/car/'
   # 경로 정보 요청
   r = session.get(url + loc + overview) 
   
   # 만약 경로가 안뜰 때 대체 결과 생성
   if r.status_code!= 200:
      
      status = 'undefined'
      
       # 직선 거리 계산
      distance = calculate_straight_distance(point[1], point[0], point[3], point[2]) * 1000
      
      # 경로 정보 생성 (출발지와 목적지 좌표만 포함)
      route = [[point[0], point[1]], [point[2], point[3]]]

      # 소요 시간 및 타임스탬프 계산 (가정: 보행 속도 10km/h)
      speed_km = 10#km
      speed = (speed_km * 1000/60)      
      duration = distance/speed
      
      timestamp = [0, duration]

      result = {'route': route, 'timestamp': timestamp, 'duration': duration, 'distance' : distance}
   
      return result, status
   
   # 경로 정보를 성공적으로 가져온 경우, JSON 응답을 반환
   res = r.json()   
   return res, status
# 경로를 가는데 걸리는 시간과 거리 추출 함수
def extract_duration_distance(res):
   # get_res함수에서 추출된 데이터에서 시간과 거리 뽑기
   
   duration = res['routes'][0]['duration']/(60)  # 분 단위로 변환
   distance = res['routes'][0]['distance']
   
   return duration, distance

# 경로 추출 함수
def extract_route(res):
   
    # get_res함수에서 추출된 데이터에서 경로 뽑기
    # 경로가 인코딩 되어 있기 때문에 아래 함수를 써서 디코딩해주어야지 위경도로 이루어진 경로가 나옴
    route = polyline.decode(res['routes'][0]['geometry'])
    
    # 사용할 형식에 맞춰 위경도 좌표의 위치를 바꿔주는 것
    route = list(map(lambda data: [data[1],data[0]] ,route))
    
    return route
# 총 걸리는 시간을 경로의 거리 기준으로 쪼개주는 함수
def extract_timestamp(route, duration):
    
    # 리스트를 numpy이 배열로 변경
    rt = np.array(route)
    # 리스트를 수평 기준으로 합치기
    rt = np.hstack([rt[:-1,:], rt[1:,:]])
    # 각각 직선거리 추출(리스트 형태)
    per = calculate_straight_distance(rt[:,1], rt[:,0], rt[:,3], rt[:,2])
    # 각각의 직선거리를 전체 직선거리의 합으로 나누기
    per = per / np.sum(per)

    # 계산된 비율을 기반으로 각 지점 도착 예상 시간 계산
    timestamp = per * duration
    timestamp = np.hstack([np.array([0]),timestamp])
    timestamp = list(itertools.accumulate(timestamp)) 
    
    return timestamp
# 모든 함수를 한번에 실행하는 코드(trips 데이터의 형태로 저장)
def osrm_routing_machine(O, D):

   # osrm 데이터 생성
   osrm_base, status = get_res([O.x, O.y, D.x, D.y])
   
   # osrm 데이터가 생성 됬으면 진행
   if status == 'defined':
      # 거리 및 걸리는 시간 추출
      duration, distance = extract_duration_distance(osrm_base)
      # 경로 추출
      route = extract_route(osrm_base)
      # timestamp 생성
      timestamp = extract_timestamp(route, duration)
      # 결과 저장
      result = {'route': route, 'timestamp': timestamp, 'duration': duration, 'distance' : distance}
      
      return result
   else: 
      return osrm_base
   
# OD_data 한쌍일 때 osrm_routing_machine작동함수
def osrm_routing_machine_multiprocess(OD):
   O, D = OD
   result = osrm_routing_machine(O, D)
   return result

# OD_data 데이터가 리스트쌍 일때의 osrm_routing_machine 작동함수
def osrm_routing_machine_multiprocess_all(OD_data):
    results = list(map(osrm_routing_machine_multiprocess, OD_data))
    return results

[2] OD 데이터 생성#

def get_OD_data(point, num=10):
    """
    주어진 좌표 딕셔너리(point)에서 임의의 출발지-도착지(OD) 쌍을 지정한 개수(num)만큼 생성하는 함수

    Args:
        point (dict): 지점 이름이 key, 좌표([경도, 위도])가 value인 딕셔너리
        num (int): 생성할 OD(출발-도착) 쌍의 개수

    Returns:
        list: [[O, D], [O, D], ...] 형태의 Origin-이동지점 쌍 리스트
    """
    OD_data = []  # OD(출발-도착) 쌍을 저장할 리스트

    point_keys = list(point.keys())  # 딕셔너리 key(지점명)만 리스트로 변환

    # 지정한 개수(num)만큼 OD 쌍을 반복 생성
    for _ in range(num):
        # 출발지와 도착지가 겹치지 않도록 2개를 무작위로 샘플링
        neighborhood1, neighborhood2 = rd.sample(point_keys, 2)
        # 샘플링된 지점명으로부터 각각의 좌표값 획득
        start_point = point[neighborhood1]
        end_point = point[neighborhood2]

        # 좌표값을 shapely의 Point 객체로 변환(추후 공간연산 등 활용 가능)
        O = Point(start_point)  # 출발지
        D = Point(end_point)    # 도착지

        # [출발지, 도착지] 쌍을 OD_data 리스트에 추가
        OD_data.append([O, D])

    return OD_data  # OD 쌍 리스트 반환
# 데이터 좌표
point = {
    "가천대_반도체대학" : [127.127384 , 37.450910],
    "가천대_일반대학원" : [127.130112 , 37.452589],
    "가천대_교육대학원" : [127.131698 , 37.452066],
    "가천대_학생회관" : [127.134042 , 37.453336],
    "가천대_ai_공학관" : [127.133374 , 37.455009],
}
# O는 출발지 D는 도착지로 생각하면 편함
OD_data = get_OD_data(point, 10)
OD_data
[[<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>],
 [<POINT (127 37.5)>, <POINT (127 37.5)>]]

[3] 통행 데이터 생성#

  • 출발지, 목적지가 정해졌을 때 그 사이의 경로 및 시간을 생성

# OD, DO 포인트에 대해서 각각의 trips데이터를 생성
OD_results = osrm_routing_machine_multiprocess_all(OD_data)
OD_results[0]['route'], OD_results[0]['timestamp']
([[127.13396, 37.45341],
  [127.13391, 37.45337],
  [127.13375, 37.45324],
  [127.13357, 37.45305],
  [127.13337, 37.45297],
  [127.1332, 37.45291],
  [127.13312, 37.45287],
  [127.13217, 37.45251],
  [127.13208, 37.45247],
  [127.13203, 37.45245],
  [127.13198, 37.45243],
  [127.13192, 37.45239],
  [127.13187, 37.45236],
  [127.13173, 37.45232],
  [127.13161, 37.45229],
  [127.13152, 37.45226],
  [127.13144, 37.45222],
  [127.13137, 37.45216],
  [127.13131, 37.45206],
  [127.13125, 37.45197],
  [127.13121, 37.45196],
  [127.13117, 37.45196],
  [127.13113, 37.45196],
  [127.1311, 37.45198],
  [127.13106, 37.45202],
  [127.13067, 37.45254],
  [127.13063, 37.45258],
  [127.1306, 37.45259],
  [127.13057, 37.4526],
  [127.13049, 37.45259],
  [127.13035, 37.45255],
  [127.13019, 37.45249],
  [127.13004, 37.45243],
  [127.12993, 37.45238],
  [127.1296, 37.45214],
  [127.12953, 37.45209],
  [127.12949, 37.45206],
  [127.12947, 37.45205],
  [127.12944, 37.45204],
  [127.1286, 37.45188],
  [127.12859, 37.45192],
  [127.12855, 37.45192],
  [127.12825, 37.45186],
  [127.12798, 37.45181],
  [127.12756, 37.45172],
  [127.1275, 37.45171],
  [127.12746, 37.45169],
  [127.12745, 37.45166],
  [127.12746, 37.45156],
  [127.12746, 37.45149],
  [127.12747, 37.4513],
  [127.12748, 37.45091]],
 [1.0,
  1.0261709959350807,
  1.1105800907327068,
  1.2209905377991537,
  1.303558953892394,
  1.3721506139778605,
  1.4070081408012265,
  1.7951185103074863,
  1.8331466262246754,
  1.8537888513080731,
  1.8744310807836704,
  1.9033179866448533,
  1.9264252850029249,
  1.9812827739767824,
  2.027667294173945,
  2.0636556383537927,
  2.0985133820511095,
  2.1364942555913284,
  2.187936003348364,
  2.235227055565897,
  2.2506886262295853,
  2.265436208908485,
  2.2801837915873846,
  2.294627294338715,
  2.3183463793142836,
  2.5994116045643216,
  2.623130620890384,
  2.6351266933495903,
  2.6471227644430835,
  2.6769810800404206,
  2.7318384196655012,
  2.7970786762534567,
  2.859005351484225,
  2.905738451490646,
  3.070743362870307,
  3.1054606362397252,
  3.1257488481584694,
  3.1344632995166415,
  3.146459445599264,
  3.4649485056710048,
  3.483887810075936,
  3.4986354006413363,
  3.6126984826278057,
  3.7149173689957786,
  3.875309446114456,
  3.8979131492631627,
  3.9153421111946605,
  3.9297544087261365,
  3.976342964329085,
  4.0088526719788184,
  4.097170298066176,
  4.278333333333334])

[4] timestamp 변경#

  • 지금은 모든 통행이 0분에 시작함

  • 노이즈를 추가하여 통행시간을 조정

# 원하는 범위에서 랜덤함 숫자를 원하는 만큼 뽑아내는 함수
def sample_interval(start, end, count, num_samples):
    # 시작과 끝을 count만 큼 나눔(최종 : 나눈 만큼의 숫자가 생성됨)
    interval_size = (end - start) / count
    samples = []
    # 랜덤 숫자 생성
    for i in range(count):
        interval_start = start + interval_size * i
        interval_end = interval_start + interval_size
        samples.extend(rd.sample(range(int(interval_start), int(interval_end)), num_samples))
    return samples

### ex.
# 시작 시간과 출발 시간 사이의 랜덤 숫자 생성
# 초 기준
sample_interval(0, 50, 10, 1)
[0, 5, 14, 18, 23, 25, 31, 35, 43, 49]
# 시각화 할때 겹치지 않기 하기 위해서 임의의 시간을 더해주는 함수
# 시간 범위를 정하고 싶으면 아래의  sample_interval의 앞의 두개의 인풋값 바꿔주기!
def timestamp_change(OD_results) :
    random_numbers = sample_interval(0, 50, len(OD_results), 1)
    for i in range(0, len(OD_results)) :
        # (i+1)*3을 더해주는 이유는 출발 시간이 겹치지 않기 하기 위해서
        OD_results[i]['timestamp'] = list(np.array(OD_results[i]['timestamp']) + random_numbers[i])
        
    return OD_results
# timestamp 변경
OD_results = timestamp_change(OD_results)
# 시작 시간이 잘 변경 되었나 확인
OD_results[0]['timestamp'][0], OD_results[-1]['timestamp'][0]
(1.0, 45.0)

[5] 데이터 저장#

  • 생성한 통행 데이터(OD_results)를 React 앱의 public/data/trips.json으로 저장합니다.

  • 로컬 개발 환경에서는 public/data에서 정적 파일을 제공하고, 배포 시에는 GitHub Raw 경로나 CDN에서 불러오도록 구성할 수 있습니다.

  • 이 파일은 이후 React의 TripsLayer에서 routetimestamp를 읽어 애니메이션을 그리는 핵심 데이터 소스가 됩니다.

# 데이터 저장
path = '../simulation_base/simulation/public/data/'

with open(os.path.join(path + 'trips.json'), 'w', encoding='utf-8') as file:
    json.dump(OD_results, file)

4.2 시스템 아키텍처와 설정#

목표: React 애플리케이션 설정 및 axios, mapbox-gl 같은 필수 라이브러리 설명#

  • 구성:

    • GitHub에 호스팅된 데이터를 디지털 트윈의 데이터 소스로 사용

주요 파일#

  • App.js

    • 애플리케이션의 핵심 구조와 라우트를 정의하는 메인 컴포넌트

    • 페이지 간 라우팅과 렌더링, 최상위 상태 관리

  • Trips.js

    • 경로(trips) 데이터의 시각화와 시뮬레이션 환경 설정(지도, 재질, 조명 등)

    • 시간 표시, DeckGL과 Map을 통한 지도·경로 시각화 등 주요 시각 요소 담당

    • 필요에 따라 App.js와 통합할 수 있으나, 이해를 돕기 위해 분리해 설명

  • index.js

    • React 애플리케이션의 진입점으로, HTML에 앱을 마운트하고 초기 실행 환경을 설정

    • Service Worker를 통해 오프라인 기능 및 로딩 성능을 개선할 수 있음

라이브러리#

  • Axios

    • Axios는 HTTP 요청을 보낼 수 있는 Promise 기반 라이브러리

    • React 애플리케이션에서 서버와 데이터를 주고받을 때 자주 사용됨

  • DeckGL

    • Deck.gl의 기본 컴포넌트로, 이 컴포넌트를 통해 다양한 레이어와 효과를 적용하여 데이터를 시각화함

    • 맵 스타일, 초기 뷰 설정, 레이어 등 시각화의 기본 요소를 담고 있음

    • 주로 지도 위에 경로, 포인트, 클러스터 등 다양한 시각적 요소를 표현할 때 사용함

  • ReactDOM

    • React 컴포넌트를 DOM에 렌더링할 때 사용하는 라이브러리

    • ReactDOM은 브라우저의 DOM과 직접 상호작용하는 기능을 제공하여, React 애플리케이션을 HTML 페이지에 연결하고 보여줄 수 있게 해줌

  • Service Worker

    • Service Worker는 브라우저와 네트워크 사이에서 프록시 역할을 하는 스크립트로, 웹 애플리케이션에 오프라인 기능과 푸시 알림, 백그라운드 데이터 동기화 같은 기능을 추가할 수 있게 해줌

    • 웹 애플리케이션이 더 빠르고 안정적으로 작동하도록 돕기 때문에, 특히 네트워크 상태가 불안정하거나 오프라인인 상황에서 유용함

4.2.1 Prerequisites#

이 절에서는 디지털 트윈 시뮬레이션을 위한 React 애플리케이션의 구조와 설정 방법을 다룬다. 디지털 트윈 시뮬레이션 시스템을 구축하기 위해 필요한 라이브러리 설치, 주요 파일의 역할, 데이터 소스 구성 방식을 하나씩 설명한다.

Note

** 왜 시뮬레이션의 구현을 React에서 했는가?**

React는 컴포넌트 기반 구조와 가상 DOM(Virtual DOM)을 통해 빠른 UI 업데이트를 지원하므로, 대화형 시뮬레이션을 구현하기에 적합하다. 시뮬레이션 데이터를 시간에 따라 업데이트하고, 그에 따라 화면이 실시간으로 반응하는 것이 중요한 디지털 트윈 시스템에서는 특히 유용하다.

예를 들어, 서울의 강남역 부근에서 평일 저녁 6시부터 7시 사이의 택시 수요를 시뮬레이션한다고 가정해보자. 과거 데이터를 분석해 이 시간대에 평균적으로 시간당 30명의 승객이 택시를 잡는다는 결과를 얻었습니다. 이러한 택시 수요는 포아송 분포를 따르는 경향이 있으며, 평균(λ)을 30으로 설정하면 특정 시간에 택시를 찾는 승객 수를 예측할 수 있다.

React의 컴포넌트 구조를 활용하면 이러한 시뮬레이션 데이터를 관리하고, 그에 따라 UI가 동적으로 변하게 할 수 있다. 이를 통해 사용자는 특정 시간대의 교통량이나 택시 수요 변화를 직관적으로 시각화하고 분석할 수 있다. 또한, React는 광범위한 라이브러리와 생태계를 제공하여, 지도 렌더링(mapbox-gl)이나 시뮬레이션 애니메이션(deck.gl)과 같은 추가적인 기능을 쉽게 통합할 수 있다.

React 애플리케이션을 실행하려면 Node.js가 필요하다. Node.js는 JavaScript 런타임 환경으로, 서버 측 코드와 웹 애플리케이션 빌드에 널리 사용된다.
Node.js 설치: Node.js 공식 사이트에서 운영체제에 맞는 설치 파일을 다운로드한다.
설치가 완료되면 터미널창에서 다음 명령어를 통해 설치가 잘 되었는지 확인할 수 있다.

node -v
npm -v

React에 대한 기본 개념 설명

  • React는 컴포넌트 기반의 사용자 인터페이스 라이브러리로, 애플리케이션을 작은 컴포넌트로 분리하여 모듈화하고 재사용할 수 있게 해줍니다.

  • React의 주요 목적은 복잡한 사용자 인터페이스를 효율적으로 관리하는 것이며, 상태 변화에 따라 인터페이스를 자동으로 갱신해 줍니다. 이러한 구조 덕분에 복잡한 시뮬레이션 데이터와 인터랙션이 많은 디지털 트윈 애플리케이션을 개발할 때 매우 유용합니다.

  • React는 가상 DOM(Virtual DOM)을 사용해 효율성을 높입니다. 일반 DOM과 달리 가상 DOM은 메모리상에서 변경 사항을 미리 계산한 후 실제 DOM에 반영합니다. 이는 빠른 업데이트와 렌더링을 가능하게 해, 실시간 데이터 시각화와 상호작용이 필요한 디지털 트윈 시뮬레이션에서 중요한 역할을 합니다.

4.2.2 React 애플리케이션 설정#

React 애플리케이션은 보통 Create React App을 사용해 간편하게 시작할 수 있다. Create React App은 React 프로젝트의 기본 구조를 만들어주며, 프로젝트를 개발, 테스트, 빌드, 배포하는 데 필요한 다양한 설정을 포함한다.

npx create-react-app my-simulation-app

위 명령어를 실행하면 my-simulation-app이라는 디렉터리에 기본 React 파일 구조가 생성된다. 이 구조는 다음과 같이 구성된다.

  • public/: 공개 폴더로, index.html 파일을 포함하여 웹 애플리케이션의 기본 HTML 템플릿을 제공합니다.

  • src/: React 컴포넌트, 스타일 시트, 애플리케이션 로직 등이 포함된 폴더로, 모든 개발이 이곳에서 이루어집니다.

  • package.json: 애플리케이션의 메타데이터와 의존성을 정의하는 파일입니다.

4.3 데이터 처리 및 컴포넌트 설계#

목표: 디지털 트윈 시뮬레이션에서 데이터 획득 및 관리 방식을 설명#

  • 시뮬레이션 데이터를 가져오기 위한 fetchData 함수

    • fetchData 함수는 서버나 외부 저장소에 있는 시뮬레이션 데이터를 가져와 컴포넌트에서 사용할 수 있게 하는 역할을 합니다.

    • 이 함수는 비동기적으로 JSON 파일을 로드하고, 이를 특정 컴포넌트의 상태(state)로 설정해 렌더링을 위한 데이터로 사용합니다.

    • 예를 들어, 택시 수요 시뮬레이션 데이터를 GitHub에서 가져온다고 가정할 때, fetchData 함수는 이 데이터를 앱 내의 여러 컴포넌트에 공유하고 업데이트하는 데 필요합니다.

  • 함수 예시

const fetchData = (fileName) => {
  const baseURL =
    process.env.NODE_ENV === 'production'
      ? 'https://raw.githubusercontent.com/{github_user}/{repo}/main/uam/src/data/' // 배포 시 원격 경로
      : `${process.env.PUBLIC_URL}/data/`; // 로컬 개발 시 public/data

  return axios.get(`${baseURL}${fileName}.json`).then((r) => r.data);
};
  • 배포 환경에서는 깃허브 사용자명과 저장소명을 {github_user}, {repo}에 맞게 치환하세요. 로컬 개발에서는 public/data에 있는 정적 JSON을 사용합니다.

  • 여행 데이터(JSON)의 구조와 역할

    • 여행 데이터는 시뮬레이션의 핵심 정보로, 각 이동에 대한 세부 사항을 포함합니다.

    • 예를 들어, 각 여행의 시작 및 종료 위치, 경로 좌표, 시간, 색상 등 시뮬레이션에서 필요한 요소가 이 JSON에 포함됩니다.

  • 예시

[
  {
    "id": "trip1",
    "route": [[127.1, 37.5], [127.2, 37.6]],
    "timestamp": [0, 1, 2, 3],
    "color": [255, 0, 0]
  }
]
  • 주의: JSON은 주석을 지원하지 않으므로, 설명은 문서에서 별도로 기술하세요.

  • 여행 데이터를 표시하고 업데이트하는 Trip 컴포넌트의 기능 설명

4.4 시뮬레이션 데이터 시각화#

목표: mapbox-gl을 사용해 지도 기반 데이터를 렌더링하는 방법 설명#

  • Mapbox 설정과 지도 스타일링

    • MAPBOX_TOKEN: Mapbox 서비스를 사용하기 위한 API 토큰이 필요합니다.

    • mapStyle : Mapbox의 지도 스타일 URL을 설정합니다.

  • 예시

    const MAPBOX_TOKEN = `your_mapbox_token`;
    const mapStyle = "mapbox://styles/spear5306/ckzcz5m8w002814o2coz02sjc";
  • 지도 위에 여행 데이터를 통합하고 데이터 변경을 처리하는 상태 변수 설명

    • TripsLayer: TripsLayer는 Deck.gl에서 제공하는 레이어로, 시간에 따라 경로가 변화하는 애니메이션을 구현할 수 있습니다.

    • 주요 속성:

      • data: trip 데이터를 기반으로 경로와 시간을 설정합니다.

      • getPath: route 속성을 받아 각 여행 경로의 위치를 지정합니다.

      • getTimestamps: timestamp 속성을 통해 경로가 시간에 따라 이동하도록 설정합니다.

      • currentTime: time 상태를 기반으로 현재 시간에 맞는 위치를 표시합니다.

  • 함수 예시

    const layers = [
        new TripsLayer({  
        id: 'trips',  // layer이름  
        data: trip, // 데이터가 들어갈 자리
        getPath: d => d.route, // 데이터에 구성된 경로의 key값
        getTimestamps: d => d.timestamp, // 데이터에 구성된 timestamp의 key값
        getColor: [255, 255, 0], // rgb로 색상지정 or trips에 color가 있다면 불러 올 수 있음
        opacity: 1, // 투명도
        widthMinPixels: 7, //선의 크기
        rounded: true,
        capRounded : true,
        jointRounded : true,
        trailLength : 0.5, // 선의 길이
        currentTime: time, // 시간
        shadowEnabled: false
        }),
    ];

    
  • 실시간 데이터 시각화 및 업데이트 메커니즘

    • 애니메이션 메커니즘: useCallback, useEffect를 상요해 time 상태를 주기적으로 업데이트합니다.

    • 이를 통해 여행 경로가 마치 실시간으로 이동하는 것처럼 보이게 합니다.

  • 함수 예시

    const animate = useCallback(() => {
        setTime((time) => returnAnimationTime(time));
        animation.id = window.requestAnimationFrame(animate);
    }, [animation]);

    useEffect(() => {
        animation.id = window.requestAnimationFrame(animate);
        return () => window.cancelAnimationFrame(animation.id);
    }, [animation, animate]);

4.5 인터랙티브 기능과 사용자 인터페이스#

목표: 디지털 트윈 시뮬레이션의 인터랙티브 요소 설명#

  • Splash를 사용한 로딩 상태 관리

    • App.js 에서 데이터가 불러오기까지 로딩 생성

    • 데이터가 불러와지면 components에 있는 Trips.js의 지도 등이 표시됨

  • 함수 예시

    return ( 
        <div className="container">
        {!isloaded && <Splash />}
        {isloaded && (
            <Trip trip={trip} />
        )}
        </div>
    );


  • Trip 컴포넌트 내 사용자 상호작용 설명

    • 시간 슬라이더: Slider 컴포넌트는 시뮬레이션 시간(time 변수)을 조절할 수 있는 인터랙티브 도구입니다.

    • 사용자가 슬라이더를 조작할 때마다 SliderChange 함수가 실행되며, 사용자가 선택한 시간에 맞춰 time 상태가 업데이트됩니다. 이로 인해 지도 상의 시뮬레이션 경로가 해당 시간에 맞춰 이동합니다.

      const SliderChange = (value) => {
          const time = value.target.value;
          setTime(time);
        };
        
      <Slider
              id="slider"
              value={time}
              min={minTime}
              max={maxTime}
              onChange={SliderChange}
              track="inverted"
            />
  • 지도 탐색:

    • DeckGL 컴포넌트의 controller 속성을 true로 설정하면, 사용자가 지도를 확대, 축소, 이동할 수 있는 기본적인 탐색 기능이 활성화됩니다.

      <DeckGL
              effects={DEFAULT_THEME.effects} // 지도의 조명등을 다룸
              initialViewState={INITIAL_VIEW_STATE} // 초기 지도 위치
              controller={true} // 확대 및 축소 이동 가능
              layers={layers} // 사용자가 만든 layers를 시각화
            >

            

4.6 테스트 및 배포#

목표: 테스트 방법과 애플리케이션 배포 설명#

  • 사용 가능한 테스트 프레임워크를 사용한 단위 테스트

    • 자신이 만든 시뮬레이션 맨 처음 폴더로 가서 터미널에 npm start를 입력

    • ex : simulation / src / ... 있다면 simulation폴더에서 실행

  • npm run build를 사용한 프로덕션 빌드

    • npm start 와 마찬가지로 같은 폴더에서 npm run build 실행

  • GitHub Pages에 페이지에 배포( 위와 같은 폴더에서 실행 )

    • 이를 사용하면 npm run build 사용 X

        npm add -D gh-pages
        npm run deploy # 재전송
        
  • package.json에 아래와 같은 명령어가 있는지 확인

    "scripts": {
        "start": "react-scripts start",
        "build": "react-scripts build",
        "test": "react-scripts test",
        "eject": "react-scripts eject",
        "predeploy": "npm run build",
        "deploy": "gh-pages -d build"
    },

4.7 사례 연구: 샘플 여행 시뮬레이션#

  • 목표: 특정 여행 시나리오를 시뮬레이션하며 전체 워크플로우를 데모

  • 구성:

    • 샘플 데이터(예: 여행 JSON) 설명

    • GitHub에서 화면 시각화로 데이터 흐름에 대한 단계별 설명

    • 디지털 트윈 결과의 해석 및 실제 적용 사례 설명

  • 절차 요약:

    1. 샘플 trips.json을 검토하고 최소 1개 경로를 지도에 렌더링

    2. fetchData로 데이터 로딩 → 상태에 연결하여 TripsLayer에 전달

    3. 지도 초기 상태(INITIAL_VIEW_STATE)와 Mapbox 토큰 설정

    4. 애니메이션 범위(minTime/maxTime)와 trailLength 조정으로 가시성 향상

    5. 시나리오 비교(예: 출발시간 분포 변화, OD 수 증감)에 따른 시각적 차이 분석

  • 평가 포인트:

    • 데이터 일관성(route와 timestamp 길이 정합)

    • 퍼포먼스(프레임 드랍 최소화, 과도한 레이어/폭 넓은 trailLength 지양)

    • 가독성(지도 스타일 대비, 색상 팔레트, 확대/축소 시 가독성 유지)

    • 스토리텔링(결과 해석과 실무적 인사이트 도출)

4.8 Exercise#

4.8.1 100개의 od를 뽑아서 시뮬레이션을 만들고 깃허브에 올려서 시뮬레이션 링크를 사이버캠퍼스에 제출#

# 데이터 좌표
point = {
    "중앙시장사거리" : [127.131770, 37.440888],
    "숯골사거리" : [127.142398 , 37.444055],
    "동부센트레빌2단지아파트" : [127.129460 , 37.447540],
    "수진역" : [127.140851 , 37.437443],
    "개별용달" : [127.139292 , 37.446605],
    "버거킹" : [127.150505 , 37.442235],
}

과제 지침#

  • OD 100쌍 생성: get_OD_data(point, num=100) 등으로 생성하고 osrm_routing_machine_multiprocess_all로 경로/시간 계산

  • 출발 시간 분산: timestamp_change에서 아래와 같이 수정하여 분포 확장

    • random_numbers = sample_interval(0, 300, len(OD_results), 1)

  • 데이터 저장: public/data/trips.json으로 저장하여 React에서 로드

  • 지도 중심/줌: 초기 보기(INITIAL_VIEW_STATE)에 중심 좌표 설정

    • longitude = 127.135840, latitude = 37.442836

  • 애니메이션 범위: maxTime을 최소 350 이상으로 설정(슬라이더 최대값 포함)

  • 제출물:

    • GitHub Pages 배포 링크

    • 저장소 링크(README에 실행 방법과 Mapbox 토큰 설정 방법 포함)

  • 유의 사항:

    • 좌표는 [lon, lat] 순서로 기록

    • JSON에는 주석을 넣지 않음

    • Mapbox 토큰은 .env 또는 환경변수로 관리(코드에 하드코딩 지양)