initial setting at 20-Aug-2025
This commit is contained in:
546
rog/views_apis/api_simulator.py
Executable file
546
rog/views_apis/api_simulator.py
Executable file
@ -0,0 +1,546 @@
|
||||
|
||||
|
||||
# 既存のインポート部分に追加
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rog.models import NewEvent2
|
||||
import logging
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
import math
|
||||
from haversine import haversine
|
||||
import folium
|
||||
import json
|
||||
import os
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
"""
|
||||
解説
|
||||
この実装では、ロゲイニングコースのルートシミュレーションを行う機能を提供しています:
|
||||
|
||||
1.データ入力とパラメータ設定:
|
||||
- イベントコード、コース時間、停止時間などのパラメータを受け取ります
|
||||
- 訪問すべき必須ノード(無料ノードと有料ノード)を指定できます
|
||||
- 目標速度を設定し、移動時間の計算に使用します
|
||||
2.グラフモデル構築:
|
||||
- NetworkXライブラリを使用してグラフを構築します
|
||||
- チェックポイントをノードとして、移動経路をエッジとして表現
|
||||
- 各エッジには距離と所要時間の属性を付与
|
||||
- Haversine公式を使用して緯度・経度から実際の距離を計算
|
||||
3.ルート最適化アルゴリズム:
|
||||
- 必須ノードを優先的に訪問し、残りの時間で効率良く他のノードを訪問
|
||||
- ポイント/時間の比率に基づいて効率の良いチェックポイントを選択
|
||||
- 制限時間内にゴールに戻ることを保証
|
||||
4.結果の可視化:
|
||||
- Foliumライブラリを使用して地図上にルートを可視化
|
||||
- 訪問順序、ポイント情報などを地図上に表示
|
||||
- 結果はHTML形式で保存され、ブラウザで確認可能
|
||||
5.レスポンスデータ:
|
||||
- 合計ポイント、総移動距離、総所要時間などの統計情報
|
||||
- 訪問ノード数や必須ノードの完了状況
|
||||
- 詳細なルート情報(各ステップの移動距離、所要時間、獲得ポイントなど)
|
||||
- 生成された地図へのURL
|
||||
|
||||
このシミュレーターは、ロゲイニング参加者がコース計画を立てる際に役立ちます。
|
||||
自分のペースや優先するチェックポイントに基づいて、最適なルートを事前に検討できます。
|
||||
また、イベント主催者がコースデザインの評価や、ポイント配分の調整に活用することもできます。
|
||||
"""
|
||||
|
||||
@api_view(['GET'])
|
||||
def rogaining_simulator(request):
|
||||
"""
|
||||
ロゲイニングのルートシミュレーションを実行
|
||||
|
||||
パラメータ:
|
||||
- event_code: イベントコード
|
||||
- course_time: コース時間(分)
|
||||
- pause_time_free: 無料CP停止時間(分)
|
||||
- pause_time_paid: 有料CP停止時間(分)
|
||||
- spare_time: 予備時間(分)
|
||||
- target_velocity: 目標速度(km/h)
|
||||
- free_node_to_visit: 訪問する無料ノード
|
||||
- paid_node_to_visit: 訪問する有料ノード
|
||||
"""
|
||||
logger.info("rogaining_simulator called")
|
||||
|
||||
# リクエストからパラメータを取得
|
||||
event_code = request.query_params.get('event_code')
|
||||
|
||||
try:
|
||||
course_time = float(request.query_params.get('course_time', 180)) # デフォルト3時間
|
||||
pause_time_free = float(request.query_params.get('pause_time_free', 2)) # デフォルト2分
|
||||
pause_time_paid = float(request.query_params.get('pause_time_paid', 5)) # デフォルト5分
|
||||
spare_time = float(request.query_params.get('spare_time', 30)) # デフォルト30分
|
||||
target_velocity = float(request.query_params.get('target_velocity', 5.0)) # デフォルト5km/h
|
||||
|
||||
free_node_to_visit = request.query_params.get('free_node_to_visit', '')
|
||||
paid_node_to_visit = request.query_params.get('paid_node_to_visit', '')
|
||||
|
||||
# カンマ区切りの文字列をリストに変換
|
||||
free_nodes = [n.strip() for n in free_node_to_visit.split(',') if n.strip()]
|
||||
paid_nodes = [n.strip() for n in paid_node_to_visit.split(',') if n.strip()]
|
||||
except ValueError as e:
|
||||
logger.error(f"Parameter conversion error: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": f"パラメータの変換に失敗しました: {str(e)}"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
logger.debug(f"Parameters: event_code={event_code}, course_time={course_time}, "
|
||||
f"pause_time_free={pause_time_free}, pause_time_paid={pause_time_paid}, "
|
||||
f"spare_time={spare_time}, target_velocity={target_velocity}, "
|
||||
f"free_nodes={free_nodes}, paid_nodes={paid_nodes}")
|
||||
|
||||
# パラメータ検証
|
||||
if not event_code:
|
||||
logger.warning("Missing required event_code parameter")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "イベントコードが必要です"
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
# イベントの存在確認
|
||||
event = NewEvent2.objects.filter(event_name=event_code).first()
|
||||
if not event:
|
||||
logger.warning(f"Event not found: {event_code}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "指定されたイベントが見つかりません"
|
||||
}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# チェックポイント情報の取得
|
||||
checkpoints = []
|
||||
try:
|
||||
event_cps = Location.objects.filter(event=event)
|
||||
|
||||
for cp in event_cps:
|
||||
if not cp.latitude or not cp.longitude:
|
||||
continue
|
||||
|
||||
checkpoints.append({
|
||||
'id': cp.cp_number,
|
||||
'name': cp.cp_name or cp.cp_number,
|
||||
'point': cp.cp_point or 0,
|
||||
'latitude': float(cp.latitude),
|
||||
'longitude': float(cp.longitude),
|
||||
'is_paid': cp.cp_number in paid_nodes,
|
||||
'is_free': cp.cp_number in free_nodes,
|
||||
'pause_time': pause_time_paid if cp.cp_number in paid_nodes else pause_time_free
|
||||
})
|
||||
except:
|
||||
# Locationモデルが存在しない場合、ダミーデータを作成
|
||||
logger.warning("Location model not found, using dummy data")
|
||||
checkpoints = []
|
||||
for i in range(1, 11):
|
||||
cp_number = f"CP{i}"
|
||||
checkpoints.append({
|
||||
'id': cp_number,
|
||||
'name': f"チェックポイント{i}",
|
||||
'point': i * 10,
|
||||
'latitude': 35.0 + i * 0.01,
|
||||
'longitude': 135.0 + i * 0.01,
|
||||
'is_paid': cp_number in paid_nodes,
|
||||
'is_free': cp_number in free_nodes,
|
||||
'pause_time': pause_time_paid if cp_number in paid_nodes else pause_time_free
|
||||
})
|
||||
|
||||
# スタートとゴールポイントの取得
|
||||
start_point = None
|
||||
goal_point = None
|
||||
|
||||
# 設定からスタート/ゴールポイントを取得(実際のシステムに合わせて調整)
|
||||
try:
|
||||
# イベント設定モデルからスタート/ゴール情報を取得
|
||||
from rog.models import EventSetting
|
||||
event_setting = EventSetting.objects.filter(event=event).first()
|
||||
|
||||
if event_setting:
|
||||
if all([event_setting.start_latitude, event_setting.start_longitude]):
|
||||
start_point = {
|
||||
'id': 'START',
|
||||
'name': 'スタート地点',
|
||||
'point': 0,
|
||||
'latitude': float(event_setting.start_latitude),
|
||||
'longitude': float(event_setting.start_longitude),
|
||||
'is_paid': False,
|
||||
'is_free': False,
|
||||
'pause_time': 0
|
||||
}
|
||||
|
||||
if all([event_setting.goal_latitude, event_setting.goal_longitude]):
|
||||
goal_point = {
|
||||
'id': 'GOAL',
|
||||
'name': 'ゴール地点',
|
||||
'point': 0,
|
||||
'latitude': float(event_setting.goal_latitude),
|
||||
'longitude': float(event_setting.goal_longitude),
|
||||
'is_paid': False,
|
||||
'is_free': False,
|
||||
'pause_time': 0
|
||||
}
|
||||
except:
|
||||
logger.warning("EventSetting model not found, using first CP as start/goal")
|
||||
|
||||
# スタート/ゴールが設定されていない場合、最初のCPをスタートとゴールに設定
|
||||
if not start_point and checkpoints:
|
||||
start_point = checkpoints[0].copy()
|
||||
start_point['id'] = 'START'
|
||||
start_point['name'] = 'スタート地点'
|
||||
start_point['point'] = 0
|
||||
start_point['is_paid'] = False
|
||||
start_point['is_free'] = False
|
||||
start_point['pause_time'] = 0
|
||||
|
||||
if not goal_point and checkpoints:
|
||||
goal_point = checkpoints[0].copy()
|
||||
goal_point['id'] = 'GOAL'
|
||||
goal_point['name'] = 'ゴール地点'
|
||||
goal_point['point'] = 0
|
||||
goal_point['is_paid'] = False
|
||||
goal_point['is_free'] = False
|
||||
goal_point['pause_time'] = 0
|
||||
|
||||
# ルートシミュレーション実行
|
||||
result = simulate_rogaining_route(
|
||||
checkpoints,
|
||||
start_point,
|
||||
goal_point,
|
||||
course_time=course_time,
|
||||
spare_time=spare_time,
|
||||
target_velocity=target_velocity,
|
||||
free_nodes=free_nodes,
|
||||
paid_nodes=paid_nodes
|
||||
)
|
||||
|
||||
# 結果を返す
|
||||
return Response({
|
||||
"status": "OK",
|
||||
"event_code": event_code,
|
||||
"simulation_time": timezone.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"parameters": {
|
||||
"course_time": course_time,
|
||||
"pause_time_free": pause_time_free,
|
||||
"pause_time_paid": pause_time_paid,
|
||||
"spare_time": spare_time,
|
||||
"target_velocity": target_velocity,
|
||||
"free_node_count": len(free_nodes),
|
||||
"paid_node_count": len(paid_nodes)
|
||||
},
|
||||
"result": result
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in rogaining_simulator: {str(e)}")
|
||||
return Response({
|
||||
"status": "ERROR",
|
||||
"message": "シミュレーション中にエラーが発生しました"
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
def simulate_rogaining_route(checkpoints, start_point, goal_point, course_time, spare_time, target_velocity, free_nodes, paid_nodes):
|
||||
"""
|
||||
ロゲイニングのルートをシミュレーション
|
||||
"""
|
||||
# グラフを作成
|
||||
G = nx.Graph()
|
||||
|
||||
# ノードとエッジを追加
|
||||
all_points = [start_point] + checkpoints + [goal_point]
|
||||
all_points_dict = {p['id']: p for p in all_points}
|
||||
|
||||
# ノードを追加
|
||||
for point in all_points:
|
||||
G.add_node(
|
||||
point['id'],
|
||||
name=point['name'],
|
||||
point=point['point'],
|
||||
lat=point['latitude'],
|
||||
lng=point['longitude'],
|
||||
is_paid=point['is_paid'],
|
||||
is_free=point['is_free'],
|
||||
pause_time=point['pause_time']
|
||||
)
|
||||
|
||||
# エッジを追加(全ノード間)
|
||||
for i, point1 in enumerate(all_points):
|
||||
for j, point2 in enumerate(all_points):
|
||||
if i != j:
|
||||
# 2点間の距離を計算(km)
|
||||
dist = haversine(
|
||||
(point1['latitude'], point1['longitude']),
|
||||
(point2['latitude'], point2['longitude'])
|
||||
)
|
||||
|
||||
# 時間を計算(分)
|
||||
time_min = (dist / target_velocity) * 60
|
||||
|
||||
G.add_edge(point1['id'], point2['id'], distance=dist, time=time_min)
|
||||
|
||||
# 訪問必須ノードの集合
|
||||
must_visit = set(free_nodes + paid_nodes)
|
||||
|
||||
# シミュレーション実行
|
||||
available_time = course_time - spare_time # 利用可能時間(分)
|
||||
|
||||
# 最適化アルゴリズム - ここでは貪欲法の例
|
||||
current_node = 'START'
|
||||
visited = [current_node]
|
||||
total_time = 0
|
||||
total_points = 0
|
||||
route_details = []
|
||||
|
||||
route_details.append({
|
||||
'node': current_node,
|
||||
'name': all_points_dict[current_node]['name'],
|
||||
'point': 0,
|
||||
'distance_from_prev': 0,
|
||||
'time_from_prev': 0,
|
||||
'pause_time': 0,
|
||||
'cumulative_time': 0,
|
||||
'cumulative_points': 0,
|
||||
'lat': all_points_dict[current_node]['lat'],
|
||||
'lng': all_points_dict[current_node]['lng']
|
||||
})
|
||||
|
||||
# 訪問必須ノードを先に処理
|
||||
remaining_must_visit = must_visit.copy()
|
||||
while remaining_must_visit and total_time < available_time:
|
||||
best_node = None
|
||||
best_ratio = -1
|
||||
best_time = float('inf')
|
||||
|
||||
for node in remaining_must_visit:
|
||||
if node in visited:
|
||||
continue
|
||||
|
||||
# 現在のノードから次のノードへの移動時間
|
||||
edge_time = G.edges[current_node, node]['time']
|
||||
# ノードでの停止時間
|
||||
pause_time = G.nodes[node]['pause_time']
|
||||
# 合計所要時間
|
||||
node_time = edge_time + pause_time
|
||||
|
||||
# このノードの得点
|
||||
node_point = G.nodes[node]['point']
|
||||
|
||||
# 時間あたりの得点比率(高いほど効率が良い)
|
||||
ratio = node_point / node_time if node_time > 0 else 0
|
||||
|
||||
if ratio > best_ratio:
|
||||
best_ratio = ratio
|
||||
best_node = node
|
||||
best_time = node_time
|
||||
|
||||
if best_node and total_time + best_time <= available_time:
|
||||
# 移動可能な場合、訪問
|
||||
edge_time = G.edges[current_node, best_node]['time']
|
||||
pause_time = G.nodes[best_node]['pause_time']
|
||||
distance = G.edges[current_node, best_node]['distance']
|
||||
node_point = G.nodes[best_node]['point']
|
||||
|
||||
total_time += edge_time + pause_time
|
||||
total_points += node_point
|
||||
|
||||
route_details.append({
|
||||
'node': best_node,
|
||||
'name': all_points_dict[best_node]['name'],
|
||||
'point': node_point,
|
||||
'distance_from_prev': distance,
|
||||
'time_from_prev': edge_time,
|
||||
'pause_time': pause_time,
|
||||
'cumulative_time': total_time,
|
||||
'cumulative_points': total_points,
|
||||
'lat': all_points_dict[best_node]['lat'],
|
||||
'lng': all_points_dict[best_node]['lng']
|
||||
})
|
||||
|
||||
visited.append(best_node)
|
||||
current_node = best_node
|
||||
remaining_must_visit.remove(best_node)
|
||||
else:
|
||||
# これ以上訪問できない
|
||||
break
|
||||
|
||||
# 残りの時間で他のノードを訪問(必須でないノードも含めて効率の良い順に)
|
||||
remaining_nodes = [node for node in G.nodes if node not in visited and node != 'GOAL']
|
||||
|
||||
while remaining_nodes and total_time < available_time:
|
||||
best_node = None
|
||||
best_ratio = -1
|
||||
best_time = float('inf')
|
||||
|
||||
for node in remaining_nodes:
|
||||
# 現在のノードから次のノードへの移動時間
|
||||
edge_time = G.edges[current_node, node]['time']
|
||||
# ノードでの停止時間
|
||||
pause_time = G.nodes[node]['pause_time']
|
||||
# 合計所要時間
|
||||
node_time = edge_time + pause_time
|
||||
|
||||
# このノードから最終的にゴールに行くのに必要な時間
|
||||
to_goal_time = G.edges[node, 'GOAL']['time']
|
||||
|
||||
# このノードの得点
|
||||
node_point = G.nodes[node]['point']
|
||||
|
||||
# 時間あたりの得点比率(高いほど効率が良い)
|
||||
ratio = node_point / node_time if node_time > 0 else 0
|
||||
|
||||
# ゴールまで行ける場合のみ考慮
|
||||
if total_time + node_time + to_goal_time <= available_time and ratio > best_ratio:
|
||||
best_ratio = ratio
|
||||
best_node = node
|
||||
best_time = node_time
|
||||
|
||||
if best_node:
|
||||
# 移動可能な場合、訪問
|
||||
edge_time = G.edges[current_node, best_node]['time']
|
||||
pause_time = G.nodes[best_node]['pause_time']
|
||||
distance = G.edges[current_node, best_node]['distance']
|
||||
node_point = G.nodes[best_node]['point']
|
||||
|
||||
total_time += edge_time + pause_time
|
||||
total_points += node_point
|
||||
|
||||
route_details.append({
|
||||
'node': best_node,
|
||||
'name': all_points_dict[best_node]['name'],
|
||||
'point': node_point,
|
||||
'distance_from_prev': distance,
|
||||
'time_from_prev': edge_time,
|
||||
'pause_time': pause_time,
|
||||
'cumulative_time': total_time,
|
||||
'cumulative_points': total_points,
|
||||
'lat': all_points_dict[best_node]['lat'],
|
||||
'lng': all_points_dict[best_node]['lng']
|
||||
})
|
||||
|
||||
visited.append(best_node)
|
||||
current_node = best_node
|
||||
remaining_nodes.remove(best_node)
|
||||
else:
|
||||
# これ以上訪問できない
|
||||
break
|
||||
|
||||
# ゴールまでの移動
|
||||
edge_time = G.edges[current_node, 'GOAL']['time']
|
||||
distance = G.edges[current_node, 'GOAL']['distance']
|
||||
|
||||
total_time += edge_time
|
||||
|
||||
route_details.append({
|
||||
'node': 'GOAL',
|
||||
'name': all_points_dict['GOAL']['name'],
|
||||
'point': 0,
|
||||
'distance_from_prev': distance,
|
||||
'time_from_prev': edge_time,
|
||||
'pause_time': 0,
|
||||
'cumulative_time': total_time,
|
||||
'cumulative_points': total_points,
|
||||
'lat': all_points_dict['GOAL']['lat'],
|
||||
'lng': all_points_dict['GOAL']['lng']
|
||||
})
|
||||
|
||||
visited.append('GOAL')
|
||||
|
||||
# ルートの可視化
|
||||
map_file = visualize_route(route_details, event_code=start_point['name'])
|
||||
|
||||
# 結果まとめ
|
||||
total_distance = sum(step['distance_from_prev'] for step in route_details)
|
||||
|
||||
# 必須ノードの訪問状況チェック
|
||||
unvisited_must = must_visit - set(visited)
|
||||
|
||||
# マップへのURLを生成
|
||||
map_url = f"/media/simulations/{os.path.basename(map_file)}" if map_file else None
|
||||
|
||||
result = {
|
||||
'total_points': total_points,
|
||||
'total_time': total_time,
|
||||
'total_distance': total_distance,
|
||||
'visited_nodes_count': len(visited) - 2, # スタートとゴールを除く
|
||||
'must_visit_completed': len(unvisited_must) == 0,
|
||||
'unvisited_must_nodes': list(unvisited_must),
|
||||
'route': route_details,
|
||||
'map_url': map_url
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def visualize_route(route_details, event_code='rogaining'):
|
||||
"""
|
||||
ルートを可視化して地図を生成
|
||||
"""
|
||||
try:
|
||||
# ルートの中心位置を計算
|
||||
lats = [step['lat'] for step in route_details]
|
||||
lngs = [step['lng'] for step in route_details]
|
||||
center_lat = sum(lats) / len(lats)
|
||||
center_lng = sum(lngs) / len(lngs)
|
||||
|
||||
# 地図を作成
|
||||
m = folium.Map(location=[center_lat, center_lng], zoom_start=13)
|
||||
|
||||
# ルートをプロット
|
||||
route_points = [(step['lat'], step['lng']) for step in route_details]
|
||||
folium.PolyLine(
|
||||
route_points,
|
||||
color='blue',
|
||||
weight=3,
|
||||
opacity=0.7
|
||||
).add_to(m)
|
||||
|
||||
# スタート地点にマーカーを追加
|
||||
folium.Marker(
|
||||
[route_details[0]['lat'], route_details[0]['lng']],
|
||||
popup='スタート',
|
||||
icon=folium.Icon(color='green', icon='play')
|
||||
).add_to(m)
|
||||
|
||||
# ゴール地点にマーカーを追加
|
||||
folium.Marker(
|
||||
[route_details[-1]['lat'], route_details[-1]['lng']],
|
||||
popup='ゴール',
|
||||
icon=folium.Icon(color='red', icon='stop')
|
||||
).add_to(m)
|
||||
|
||||
# 各チェックポイントにマーカーを追加
|
||||
for i, step in enumerate(route_details[1:-1], 1):
|
||||
folium.Marker(
|
||||
[step['lat'], step['lng']],
|
||||
popup=f"{i}. {step['name']} ({step['point']}pts)",
|
||||
icon=folium.Icon(color='blue')
|
||||
).add_to(m)
|
||||
|
||||
# 訪問順序を表示
|
||||
folium.Circle(
|
||||
[step['lat'], step['lng']],
|
||||
radius=10,
|
||||
color='black',
|
||||
fill=True,
|
||||
fill_color='white',
|
||||
fill_opacity=0.7,
|
||||
popup=str(i)
|
||||
).add_to(m)
|
||||
|
||||
# 保存先ディレクトリを確保
|
||||
sim_dir = os.path.join(settings.MEDIA_ROOT, 'simulations')
|
||||
os.makedirs(sim_dir, exist_ok=True)
|
||||
|
||||
# タイムスタンプを含むファイル名
|
||||
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
|
||||
filename = f"route_sim_{event_code}_{timestamp}.html"
|
||||
file_path = os.path.join(sim_dir, filename)
|
||||
|
||||
# 地図を保存
|
||||
m.save(file_path)
|
||||
|
||||
return file_path
|
||||
except Exception as e:
|
||||
logger.error(f"Error visualizing route: {str(e)}")
|
||||
return None
|
||||
Reference in New Issue
Block a user