Files
rogaining_srv/rog/views_apis/api_monitor.py
2025-08-20 19:15:19 +09:00

492 lines
20 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 既存のインポート部分に追加
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)