# 既存のインポート部分に追加 from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework import status from rog.models import NewEvent2, Entry, Location, GpsLog import logging from django.db.models import F, Q from django.conf import settings import os from urllib.parse import urljoin logger = logging.getLogger(__name__) """ 解説 この実装では以下の処理を行っています: 1.イベントコードとクラス名のパラメータを受け取ります。省略可能なパラメータとして limit(表示するトップ選手の数)も受け取ります。 2.パラメータが不足している場合はエラーを返します。 3.指定されたイベントの存在を確認します。 4.指定されたクラスに所属する、ゴール済みのチームをスコア降順で取得します。 5.上位 limit 件(デフォルトは3件)のチームについて、詳細なルート情報を収集します: - チームの基本情報(名前、ゼッケン番号、クラス、順位、スコア) - スタート情報とゴール情報 - ウェイポイントとチェックポイントのデータ - ルートの統計情報(総距離、ウェイポイント数、チェックポイント数) 6.各チームに特徴的な色を割り当て(1位: 金色、2位: 銀色、3位: 銅色)、ルートを区別しやすくします。 7.全チームの情報と、GeoJSON形式のデータを含む応答を返します。 実装のポイント: - /getAllRoutes とは異なり、上位チームのみを対象としています。 - 上位チームは詳細なルートポイント情報も含めることで、より詳しい分析が可能です。 - 各チームのルートを視覚的に区別しやすくするための色情報を追加しています。 - チェックポイントもGeoJSON内のポイントフィーチャーとして追加し、各チームがどのチェックポイントを通過したかを可視化しやすくしています。 このエンドポイントは、結果分析や表彰式でのプレゼンテーション、上位選手のルート戦略の研究などに役立ちます。 """ from math import sin, cos, sqrt, atan2, radians logger = logging.getLogger(__name__) @api_view(['GET']) def top_users_routes(request): """ 指定クラスのトップ選手のルート情報を取得 パラメータ: - event_code: イベントコード - class_name: クラス名 - limit: 取得するトップ選手の数(省略時は3) """ logger.info("top_users_routes called") # リクエストからパラメータを取得 event_code = request.query_params.get('event_code') class_name = request.query_params.get('class_name') # クエリパラメータの別名対応(互換性のため) if not event_code: event_code = request.query_params.get('event') # 省略可能なパラメータ try: limit = int(request.query_params.get('limit', 3)) except ValueError: limit = 3 logger.debug(f"Parameters: event_code={event_code}, class_name={class_name}, limit={limit}") # パラメータ検証 if not all([event_code, class_name]): logger.warning("Missing required parameters") 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) # ゴール済みで指定クラスに属する上位チームを取得 top_entries = Entry.objects.filter( event=event, class_name=class_name, goal_info__isnull=False # ゴール済みのチームのみ ).order_by('-goal_info__score')[:limit] # スコア降順で上位limit件 if not top_entries.exists(): logger.warning(f"No teams with goal info found for event: {event_code}, class: {class_name}") return Response({ "status": "WARNING", "message": "ゴール済みのチームが見つかりません", "teams": [], "total_teams": 0 }) # トップチームの情報とルートを取得 teams_data = [] geo_json_features = [] for entry in top_entries: try: # ウェイポイントデータを取得(時間順) waypoints = Waypoint.objects.filter( entry=entry ).order_by('recorded_at') # チェックポイント通過情報を取得(時間順) checkpoints = GpsLog.objects.filter( entry=entry ).order_by('checkin_time') # スタート情報を取得 start_info = None if hasattr(entry, 'start_info'): start_info = { "start_time": entry.start_info.start_time.strftime("%Y-%m-%d %H:%M:%S") } # ゴール情報を取得 goal_info = None if hasattr(entry, 'goal_info'): goal_info = { "goal_time": entry.goal_info.goal_time.strftime("%Y-%m-%d %H:%M:%S"), "score": entry.goal_info.score } # ウェイポイントとチェックポイントを処理 route_points = [] # ウェイポイントを処理 for wp in waypoints: point = { "latitude": wp.latitude, "longitude": wp.longitude, "timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"), "type": "waypoint" } if wp.altitude is not None: point["altitude"] = wp.altitude if wp.accuracy is not None: point["accuracy"] = wp.accuracy if wp.speed is not None: point["speed"] = wp.speed route_points.append(point) # チェックポイントの座標を追加(イベントのチェックポイント定義がある場合) checkpoint_data = [] for cp in checkpoints: cp_data = { "cp_number": cp.cp_number, "checkin_time": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None, "is_service_checked": getattr(cp, 'is_service_checked', False) } # チェックポイントの座標情報を取得 try: event_cp = Location.objects.filter( event=event, cp_number=cp.cp_number ).first() if event_cp and event_cp.latitude and event_cp.longitude: cp_data["latitude"] = event_cp.latitude cp_data["longitude"] = event_cp.longitude cp_data["cp_name"] = event_cp.cp_name cp_data["cp_point"] = event_cp.cp_point # ルートポイントとしても追加 route_points.append({ "latitude": event_cp.latitude, "longitude": event_cp.longitude, "timestamp": cp.checkin_time.strftime("%Y-%m-%d %H:%M:%S") if cp.checkin_time else None, "type": "checkpoint", "cp_number": cp.cp_number }) except: # Locationモデルが存在しないか、座標が設定されていない場合 pass checkpoint_data.append(cp_data) # ルートの統計情報を計算 total_distance = 0 if len(route_points) >= 2: def calculate_distance(lat1, lon1, lat2, lon2): # 2点間の距離をハバーサイン公式で計算(km単位) R = 6371.0 # 地球の半径(km) lat1_rad = radians(lat1) lon1_rad = radians(lon1) lat2_rad = radians(lat2) lon2_rad = radians(lon2) dlon = lon2_rad - lon1_rad dlat = lat2_rad - lat1_rad a = sin(dlat / 2)**2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2)**2 c = 2 * atan2(sqrt(a), sqrt(1 - a)) distance = R * c return distance for i in range(len(route_points) - 1): p1 = route_points[i] p2 = route_points[i + 1] if 'latitude' in p1 and 'longitude' in p1 and 'latitude' in p2 and 'longitude' in p2: distance = calculate_distance( p1['latitude'], p1['longitude'], p2['latitude'], p2['longitude'] ) total_distance += distance # チームデータを追加 team_data = { "rank": list(top_entries).index(entry) + 1, # 順位 "team_name": entry.team_name, "zekken_number": entry.zekken_number, "class_name": entry.class_name, "score": goal_info["score"] if goal_info else None, "start_info": start_info, "goal_info": goal_info, "route_statistics": { "total_distance": round(total_distance, 2), "total_waypoints": waypoints.count(), "total_checkpoints": checkpoints.count() }, "checkpoints": checkpoint_data, "route_points": route_points # トップチームなので詳細なルートデータも含める } teams_data.append(team_data) # GeoJSONフィーチャーを追加 if route_points: # 特徴的な色をチームに割り当て(上位3チームの場合、金・銀・銅のイメージ) colors = ["#FFD700", "#C0C0C0", "#CD7F32"] # Gold, Silver, Bronze rank = list(top_entries).index(entry) color = colors[rank] if rank < len(colors) else f"#{(rank * 50) % 256:02x}{100:02x}{150:02x}" geo_json_features.append({ "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [point["longitude"], point["latitude"]] for point in route_points if "latitude" in point and "longitude" in point ] }, "properties": { "team_name": entry.team_name, "zekken_number": entry.zekken_number, "class_name": entry.class_name, "rank": list(top_entries).index(entry) + 1, "score": goal_info["score"] if goal_info else None, "total_distance": round(total_distance, 2), "color": color, "waypoint_count": waypoints.count(), "checkpoint_count": checkpoints.count() } }) # チェックポイントをポイントフィーチャーとして追加 for cp in checkpoint_data: if 'latitude' in cp and 'longitude' in cp: geo_json_features.append({ "type": "Feature", "geometry": { "type": "Point", "coordinates": [cp["longitude"], cp["latitude"]] }, "properties": { "team_name": entry.team_name, "zekken_number": entry.zekken_number, "type": "checkpoint", "cp_number": cp["cp_number"], "cp_name": cp.get("cp_name", cp["cp_number"]), "color": color, "rank": list(top_entries).index(entry) + 1 } }) except Exception as e: logger.error(f"Error processing route for team {entry.team_name}: {str(e)}") # エラーが発生したチームはスキップ continue # 対象クラスの全チーム数(ゴール済み)を取得 total_teams_in_class = Entry.objects.filter( event=event, class_name=class_name, goal_info__isnull=False ).count() # GeoJSON形式のデータを作成 geo_json = { "type": "FeatureCollection", "features": geo_json_features } return Response({ "status": "OK", "event_code": event_code, "class_name": class_name, "top_limit": limit, "total_teams_in_class": total_teams_in_class, "total_top_teams": len(teams_data), "teams": teams_data, "geo_json": geo_json }) except Exception as e: logger.error(f"Error in top_users_routes: {str(e)}") return Response({ "status": "ERROR", "message": "サーバーエラーが発生しました" }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) """ pip install folium selenium webdriver-manager Pillow """ # 既存のインポート部分に追加 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 import os import tempfile import time import folium from folium.plugins import MarkerCluster from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from django.conf import settings from django.core.files.storage import default_storage from django.core.files.base import ContentFile from PIL import Image import uuid from pathlib import Path from django.utils import timezone """ 解説 この実装では以下の処理を行っています: 1.イベントコードとゼッケン番号のパラメータを受け取ります 2.パラメータが不足している場合はエラーを返します 3.指定されたイベントとチームの存在を確認します 4.チームのウェイポイントとチェックポイントデータを取得します 5.Foliumライブラリを使用して地図を生成し、以下の情報を描画します: - ルート(青い線) - スタート地点(緑のマーカー) - 現在地点/ゴール地点(赤のマーカー) - チェックポイント(緑のフラグ:通過済み、グレーのフラグ:未通過) - チーム情報(名前、ゼッケン番号、クラス、スタート時間、ゴール時間、スコア) 6.生成した地図をHTMLとして一時保存します 7.Seleniumを使用して、HTMLのスクリーンショットを撮影しPNG画像を生成します 8.画像をメディアディレクトリに保存し、URLを生成します 9.画像のURLを含むレスポンスを返します 実装のポイント: - Foliumライブラリを使用することで、OpenStreetMapをベースにした高品質な地図を生成できます - Seleniumでのスクリーンショット撮影により、インタラクティブな地図を静的画像として保存できます - チェックポイントの通過状況に応じてマーカーの色を変えることで、視覚的な情報を提供します - チーム情報を地図上に表示することで、画像だけで必要な情報が分かるようにしています 注意点: - この実装にはSeleniumとChromeドライバーが必要です。サーバー環境によっては追加の設定が必要になる場合があります - 画像生成は比較的重い処理なので、リクエストが多い場合はキャッシュ機能の実装を検討するとよいでしょう このエンドポイントにより、チームの移動ルートを可視化した画像を生成し、ウェブページに埋め込んだり、 SNSで共有したりすることができます。 """ logger = logging.getLogger(__name__) @api_view(['GET']) def generate_route_image(request): """ チームのルートを可視化した画像を生成 パラメータ: - event_code: イベントコード - zekken_number: ゼッケン番号 """ logger.info("generate_route_image called") # リクエストからパラメータを取得 event_code = request.query_params.get('event_code') zekken_number = request.query_params.get('zekken_number') # 別名のパラメータ対応 if not event_code: event_code = request.query_params.get('event') if not zekken_number: zekken_number = request.query_params.get('zekken') logger.debug(f"Parameters: event_code={event_code}, zekken_number={zekken_number}") # パラメータ検証 if not all([event_code, zekken_number]): logger.warning("Missing required parameters") 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) # ウェイポイントデータを取得(時間順) waypoints = Waypoint.objects.filter( entry=entry ).order_by('recorded_at') if not waypoints.exists(): logger.warning(f"No waypoints found for team: {entry.team_name}") return Response({ "status": "WARNING", "message": "このチームのウェイポイントデータがありません" }, status=status.HTTP_200_OK) # チェックポイント通過情報を取得(時間順) checkpoints = GpsLog.objects.filter( entry=entry ).order_by('checkin_time') # チェックポイント情報の取得(座標) cp_locations = {} try: event_cps = Location.objects.filter(event=event) for cp in event_cps: if cp.latitude is not None and cp.longitude is not None: cp_locations[cp.cp_number] = { 'lat': cp.latitude, 'lon': cp.longitude, 'name': cp.cp_name, 'point': cp.cp_point } except: # Locationモデルが存在しない場合 pass # チームが通過したチェックポイント番号のリスト visited_cps = set([cp.cp_number for cp in checkpoints]) # 地図の中心座標を計算(全ウェイポイントの中央値) lats = [wp.latitude for wp in waypoints] lons = [wp.longitude for wp in waypoints] center_lat = sum(lats) / len(lats) center_lon = sum(lons) / len(lons) # スタート地点と終了地点 start_point = (waypoints.first().latitude, waypoints.first().longitude) end_point = (waypoints.last().latitude, waypoints.last().longitude) # Folium地図を作成 m = folium.Map(location=[center_lat, center_lon], zoom_start=13) # ルートを描画 route_points = [(wp.latitude, wp.longitude) for wp in waypoints] folium.PolyLine( route_points, color='blue', weight=3, opacity=0.7 ).add_to(m) # スタート地点をマーク folium.Marker( start_point, popup="スタート", icon=folium.Icon(color='green', icon='play', prefix='fa') ).add_to(m) # 終了地点をマーク folium.Marker( end_point, popup="現在位置 / ゴール", icon=folium.Icon(color='red', icon='stop', prefix='fa') ).add_to(m) # チェックポイントを描画 for cp_number, cp_info in cp_locations.items(): visited = cp_number in visited_cps color = 'green' if visited else 'gray' folium.Marker( [cp_info['lat'], cp_info['lon']], popup=f"{cp_info['name']} ({cp_info['point']}点) - {'通過済' if visited else '未通過'}", icon=folium.Icon(color=color, icon='flag', prefix='fa') ).add_to(m) # チーム情報を追加 team_info = f"""
チーム名: {entry.team_name}
ゼッケン番号: {entry.zekken_number}
クラス: {entry.class_name}
""" if hasattr(entry, 'start_info'): team_info += f"スタート時間: {entry.start_info.start_time.strftime('%Y-%m-%d %H:%M:%S')}
" if hasattr(entry, 'goal_info'): team_info += f"""ゴール時間: {entry.goal_info.goal_time.strftime('%Y-%m-%d %H:%M:%S')}
スコア: {entry.goal_info.score}点
""" team_info += "