initial setting at 20-Aug-2025

This commit is contained in:
2025-08-20 19:15:19 +09:00
parent eab529bd3b
commit 1ba305641e
149 changed files with 170449 additions and 1802 deletions

491
rog/views_apis/api_monitor.py Executable file
View File

@ -0,0 +1,491 @@
# 既存のインポート部分に追加
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)