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