492 lines
20 KiB
Python
Executable File
492 lines
20 KiB
Python
Executable File
# 既存のインポート部分に追加
|
||
from rest_framework.decorators import api_view
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rog.models import NewEvent2, Entry, Waypoint, GpsLog
|
||
import logging
|
||
from django.db.models import F, Q, Max, Count
|
||
from datetime import datetime, timedelta
|
||
from django.utils import timezone
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
"""
|
||
解説
|
||
この実装では2つのエンドポイントを提供しています:
|
||
|
||
1. /realtimeMonitor (全チーム対象)
|
||
|
||
すべてのチームの最新位置情報を取得するエンドポイントです。
|
||
|
||
- パラメータ:
|
||
- event_code:イベントコード(必須)
|
||
- class:クラス名(省略可)
|
||
|
||
主な機能:
|
||
|
||
- スタート済みの全チームの最新位置情報を返す
|
||
- クラス名でフィルタリングできる
|
||
- 各チームの状態(未スタート、競技中、ゴール済)、経過時間、スコア、チェックポイント数などの情報を提供
|
||
- 位置情報の「鮮度」を評価(5分以内は「新鮮」、5分~30分は「通常」、30分以上は「古い」)
|
||
- GeoJSON形式のデータも提供し、地図表示に使いやすくしている
|
||
|
||
2. /realtimeMonitor_zekken_narrow (特定チーム対象)
|
||
|
||
特定のゼッケン番号を持つチームの詳細な位置情報を取得するエンドポイントです。
|
||
|
||
- パラメータ:
|
||
- event_code:イベントコード(必須)
|
||
- zekken:ゼッケン番号(必須)
|
||
- class:クラス名(省略可)
|
||
|
||
- 主な機能:
|
||
- 指定されたゼッケン番号を持つチームの詳細データを返す
|
||
- 最新の位置情報だけでなく、複数の過去の位置情報も提供(最新10件)
|
||
- クラス名が指定された場合、そのクラスに所属しているか確認する
|
||
- チームの状態、経過時間、スコア、チェックポイント数などの詳細情報を提供
|
||
- GeoJSON形式で以下のデータを提供:
|
||
- 最新位置(特別なマーカー)
|
||
- 過去の位置(別のマーカー)
|
||
- 移動経路(ライン)
|
||
|
||
この2つのエンドポイントにより、ウェブ上でリアルタイムのモニタリングダッシュボードを実現できます。
|
||
全チームの概要を一覧表示し、特定チームを選択すると詳細な移動履歴を確認できる仕組みです。
|
||
"""
|
||
|
||
@api_view(['GET'])
|
||
def realtime_monitor(request):
|
||
"""
|
||
リアルタイムのチーム位置情報を取得
|
||
|
||
パラメータ:
|
||
- event_code: イベントコード
|
||
- class: クラス名(省略可)
|
||
"""
|
||
logger.info("realtime_monitor called")
|
||
|
||
# リクエストからパラメータを取得
|
||
event_code = request.query_params.get('event_code')
|
||
class_name = request.query_params.get('class')
|
||
|
||
# パラメータの別名対応
|
||
if not event_code:
|
||
event_code = request.query_params.get('event')
|
||
|
||
logger.debug(f"Parameters: event_code={event_code}, class={class_name}")
|
||
|
||
# パラメータ検証
|
||
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)
|
||
|
||
# スタート済みのチームをクエリ
|
||
entries_query = Entry.objects.filter(
|
||
event=event,
|
||
start_info__isnull=False
|
||
)
|
||
|
||
# クラス名でフィルタリング(指定があれば)
|
||
if class_name:
|
||
entries_query = entries_query.filter(class_name=class_name)
|
||
|
||
# 各チームの最新ウェイポイントを取得するためのサブクエリ
|
||
subquery = Waypoint.objects.filter(
|
||
entry=OuterRef('pk')
|
||
).order_by('-recorded_at').values('latitude', 'longitude', 'recorded_at', 'speed', 'altitude')[:1]
|
||
|
||
# 各チームのチェックポイント数を取得するためのサブクエリ
|
||
cp_count_subquery = GpsLog.objects.filter(
|
||
entry=OuterRef('pk')
|
||
).values('entry').annotate(count=Count('id')).values('count')
|
||
|
||
# アノテーションを追加してサブクエリの結果をメインクエリに結合
|
||
entries_with_latest_waypoint = entries_query.annotate(
|
||
latest_latitude=Subquery(subquery.values('latitude')[:1]),
|
||
latest_longitude=Subquery(subquery.values('longitude')[:1]),
|
||
latest_recorded_at=Subquery(subquery.values('recorded_at')[:1]),
|
||
latest_speed=Subquery(subquery.values('speed')[:1]),
|
||
latest_altitude=Subquery(subquery.values('altitude')[:1]),
|
||
checkpoint_count=Coalesce(Subquery(cp_count_subquery), 0)
|
||
).filter(
|
||
# 最新ウェイポイントがあるチームのみ
|
||
latest_latitude__isnull=False
|
||
)
|
||
|
||
# 現在時刻を取得
|
||
now = timezone.now()
|
||
|
||
# 各チームのリアルタイム情報を収集
|
||
teams_data = []
|
||
|
||
for entry in entries_with_latest_waypoint:
|
||
# ウェイポイントがないチームはスキップ
|
||
if not hasattr(entry, 'latest_latitude') or not entry.latest_latitude:
|
||
continue
|
||
|
||
# 最新の位置情報の取得時刻
|
||
location_time = entry.latest_recorded_at
|
||
location_age_seconds = (now - location_time).total_seconds() if location_time else None
|
||
|
||
# 位置情報の鮮度を評価(5分以内は新鮮、30分以上は古い)
|
||
location_freshness = "fresh" if location_age_seconds and location_age_seconds < 300 else \
|
||
"normal" if location_age_seconds and location_age_seconds < 1800 else "stale"
|
||
|
||
# スタート、ゴール情報を取得
|
||
start_time = entry.start_info.start_time if hasattr(entry, 'start_info') else None
|
||
goal_time = entry.goal_info.goal_time if hasattr(entry, 'goal_info') else None
|
||
score = entry.goal_info.score if hasattr(entry, 'goal_info') and hasattr(entry.goal_info, 'score') else None
|
||
|
||
# 経過時間を計算
|
||
elapsed_time = None
|
||
if start_time:
|
||
end_time = goal_time if goal_time else now
|
||
elapsed_seconds = (end_time - start_time).total_seconds()
|
||
|
||
# 時間:分:秒の形式にフォーマット
|
||
hours, remainder = divmod(int(elapsed_seconds), 3600)
|
||
minutes, seconds = divmod(remainder, 60)
|
||
elapsed_time = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||
|
||
# チームステータスを判定
|
||
if goal_time:
|
||
status_text = "ゴール済"
|
||
elif start_time:
|
||
status_text = "競技中"
|
||
else:
|
||
status_text = "未スタート"
|
||
|
||
team_data = {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": entry.zekken_number,
|
||
"class_name": entry.class_name,
|
||
"status": status_text,
|
||
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
|
||
"goal_time": goal_time.strftime("%Y-%m-%d %H:%M:%S") if goal_time else None,
|
||
"elapsed_time": elapsed_time,
|
||
"score": score,
|
||
"checkpoint_count": entry.checkpoint_count,
|
||
"location": {
|
||
"latitude": entry.latest_latitude,
|
||
"longitude": entry.latest_longitude,
|
||
"timestamp": location_time.strftime("%Y-%m-%d %H:%M:%S") if location_time else None,
|
||
"age_seconds": int(location_age_seconds) if location_age_seconds else None,
|
||
"freshness": location_freshness,
|
||
"speed": entry.latest_speed,
|
||
"altitude": entry.latest_altitude
|
||
}
|
||
}
|
||
|
||
teams_data.append(team_data)
|
||
|
||
# GeoJSON形式のデータを作成
|
||
geo_json_features = []
|
||
|
||
# 各チームのポイントフィーチャーを追加
|
||
for team in teams_data:
|
||
# 位置情報がないチームはスキップ
|
||
if not team['location']['latitude'] or not team['location']['longitude']:
|
||
continue
|
||
|
||
# 位置情報の鮮度に基づいて色を変更
|
||
color = {
|
||
"fresh": "#00FF00", # 緑
|
||
"normal": "#FFFF00", # 黄
|
||
"stale": "#FF0000" # 赤
|
||
}.get(team['location']['freshness'], "#AAAAAA") # デフォルトはグレー
|
||
|
||
# ゴール済みのチームは異なるマーカーを使用
|
||
is_goal = team['status'] == "ゴール済"
|
||
|
||
geo_json_features.append({
|
||
"type": "Feature",
|
||
"geometry": {
|
||
"type": "Point",
|
||
"coordinates": [team['location']['longitude'], team['location']['latitude']]
|
||
},
|
||
"properties": {
|
||
"team_name": team['team_name'],
|
||
"zekken_number": team['zekken_number'],
|
||
"class_name": team['class_name'],
|
||
"status": team['status'],
|
||
"elapsed_time": team['elapsed_time'],
|
||
"score": team['score'],
|
||
"checkpoint_count": team['checkpoint_count'],
|
||
"timestamp": team['location']['timestamp'],
|
||
"age_seconds": team['location']['age_seconds'],
|
||
"color": color,
|
||
"icon": "flag-checkered" if is_goal else "map-marker-alt",
|
||
"iconColor": color
|
||
}
|
||
})
|
||
|
||
geo_json = {
|
||
"type": "FeatureCollection",
|
||
"features": geo_json_features
|
||
}
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"event_code": event_code,
|
||
"class_name": class_name,
|
||
"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||
"total_teams": len(teams_data),
|
||
"teams": teams_data,
|
||
"geo_json": geo_json
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in realtime_monitor: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
@api_view(['GET'])
|
||
def realtime_monitor_zekken_narrow(request):
|
||
"""
|
||
指定ゼッケン番号のチームの位置情報を取得
|
||
|
||
パラメータ:
|
||
- event_code: イベントコード
|
||
- class: クラス名(省略可)
|
||
- zekken: ゼッケン番号
|
||
"""
|
||
logger.info("realtime_monitor_zekken_narrow called")
|
||
|
||
# リクエストからパラメータを取得
|
||
event_code = request.query_params.get('event_code')
|
||
class_name = request.query_params.get('class')
|
||
zekken_number = request.query_params.get('zekken')
|
||
|
||
# パラメータの別名対応
|
||
if not event_code:
|
||
event_code = request.query_params.get('event')
|
||
|
||
logger.debug(f"Parameters: event_code={event_code}, class={class_name}, zekken={zekken_number}")
|
||
|
||
# パラメータ検証
|
||
if not event_code:
|
||
logger.warning("Missing required event_code parameter")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "イベントコードが必要です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if not zekken_number:
|
||
logger.warning("Missing required zekken 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)
|
||
|
||
# 指定ゼッケン番号のチームを検索
|
||
entry = Entry.objects.filter(
|
||
event=event,
|
||
zekken_number=zekken_number
|
||
).first()
|
||
|
||
if not entry:
|
||
logger.warning(f"Team with zekken number {zekken_number} not found in event: {event_code}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "指定されたゼッケン番号のチームが見つかりません"
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# クラス名でフィルタリング(指定があれば)
|
||
if class_name and entry.class_name != class_name:
|
||
logger.warning(f"Team {zekken_number} is not in class: {class_name}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "指定されたクラスに所属していません"
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# ウェイポイントデータを取得(最新の複数件)
|
||
waypoints = Waypoint.objects.filter(
|
||
entry=entry
|
||
).order_by('-recorded_at')[:10] # 最新10件
|
||
|
||
# チェックポイント数を取得
|
||
checkpoint_count = GpsLog.objects.filter(entry=entry).count()
|
||
|
||
# 現在時刻を取得
|
||
now = timezone.now()
|
||
|
||
# スタート、ゴール情報を取得
|
||
start_time = entry.start_info.start_time if hasattr(entry, 'start_info') else None
|
||
goal_time = entry.goal_info.goal_time if hasattr(entry, 'goal_info') and entry.goal_info else None
|
||
score = entry.goal_info.score if hasattr(entry, 'goal_info') and entry.goal_info and hasattr(entry.goal_info, 'score') else None
|
||
|
||
# 経過時間を計算
|
||
elapsed_time = None
|
||
if start_time:
|
||
end_time = goal_time if goal_time else now
|
||
elapsed_seconds = (end_time - start_time).total_seconds()
|
||
|
||
# 時間:分:秒の形式にフォーマット
|
||
hours, remainder = divmod(int(elapsed_seconds), 3600)
|
||
minutes, seconds = divmod(remainder, 60)
|
||
elapsed_time = f"{hours:02}:{minutes:02}:{seconds:02}"
|
||
|
||
# チームステータスを判定
|
||
if goal_time:
|
||
status_text = "ゴール済"
|
||
elif start_time:
|
||
status_text = "競技中"
|
||
else:
|
||
status_text = "未スタート"
|
||
|
||
# ウェイポイントデータを処理
|
||
location_data = []
|
||
latest_location = None
|
||
|
||
for i, wp in enumerate(waypoints):
|
||
location = {
|
||
"latitude": wp.latitude,
|
||
"longitude": wp.longitude,
|
||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||
"age_seconds": int((now - wp.recorded_at).total_seconds()),
|
||
"speed": wp.speed,
|
||
"altitude": wp.altitude,
|
||
"accuracy": wp.accuracy
|
||
}
|
||
|
||
# 位置情報の鮮度を評価
|
||
location["freshness"] = "fresh" if location["age_seconds"] < 300 else \
|
||
"normal" if location["age_seconds"] < 1800 else "stale"
|
||
|
||
location_data.append(location)
|
||
|
||
# 最新位置情報を保存
|
||
if i == 0:
|
||
latest_location = location
|
||
|
||
# GeoJSON形式のデータを作成
|
||
geo_json_features = []
|
||
|
||
# 最新位置のポイントフィーチャー
|
||
if latest_location:
|
||
color = {
|
||
"fresh": "#00FF00", # 緑
|
||
"normal": "#FFFF00", # 黄
|
||
"stale": "#FF0000" # 赤
|
||
}.get(latest_location['freshness'], "#AAAAAA") # デフォルトはグレー
|
||
|
||
is_goal = status_text == "ゴール済"
|
||
|
||
geo_json_features.append({
|
||
"type": "Feature",
|
||
"geometry": {
|
||
"type": "Point",
|
||
"coordinates": [latest_location['longitude'], latest_location['latitude']]
|
||
},
|
||
"properties": {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": zekken_number,
|
||
"class_name": entry.class_name,
|
||
"status": status_text,
|
||
"timestamp": latest_location['timestamp'],
|
||
"color": color,
|
||
"icon": "flag-checkered" if is_goal else "map-marker-alt",
|
||
"iconColor": color,
|
||
"isLatest": True
|
||
}
|
||
})
|
||
|
||
# 過去位置のポイントフィーチャー
|
||
for i, location in enumerate(location_data):
|
||
if i == 0: # 最新位置は既に追加済み
|
||
continue
|
||
|
||
geo_json_features.append({
|
||
"type": "Feature",
|
||
"geometry": {
|
||
"type": "Point",
|
||
"coordinates": [location['longitude'], location['latitude']]
|
||
},
|
||
"properties": {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": zekken_number,
|
||
"timestamp": location['timestamp'],
|
||
"color": "#AAAAAA", # 過去の位置はグレー
|
||
"icon": "circle",
|
||
"iconColor": "#AAAAAA",
|
||
"isLatest": False
|
||
}
|
||
})
|
||
|
||
# 移動経路を表すラインフィーチャー
|
||
if len(location_data) >= 2:
|
||
geo_json_features.append({
|
||
"type": "Feature",
|
||
"geometry": {
|
||
"type": "LineString",
|
||
"coordinates": [
|
||
[location['longitude'], location['latitude']]
|
||
for location in reversed(location_data) # 時系列順
|
||
]
|
||
},
|
||
"properties": {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": zekken_number,
|
||
"color": "#0000FF" # 青色の線
|
||
}
|
||
})
|
||
|
||
geo_json = {
|
||
"type": "FeatureCollection",
|
||
"features": geo_json_features
|
||
}
|
||
|
||
team_data = {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": zekken_number,
|
||
"class_name": entry.class_name,
|
||
"status": status_text,
|
||
"start_time": start_time.strftime("%Y-%m-%d %H:%M:%S") if start_time else None,
|
||
"goal_time": goal_time.strftime("%Y-%m-%d %H:%M:%S") if goal_time else None,
|
||
"elapsed_time": elapsed_time,
|
||
"score": score,
|
||
"checkpoint_count": checkpoint_count,
|
||
"latest_location": latest_location,
|
||
"locations": location_data,
|
||
"locations_count": len(location_data)
|
||
}
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"event_code": event_code,
|
||
"timestamp": now.strftime("%Y-%m-%d %H:%M:%S"),
|
||
"team": team_data,
|
||
"geo_json": geo_json
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in realtime_monitor_zekken_narrow: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|