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

634
rog/views_apis/api_routes.py Executable file
View File

@ -0,0 +1,634 @@
# 既存のインポート部分に追加
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"""
<div style="font-size: 12px; font-family: Arial;">
<h3>チーム情報</h3>
<p>チーム名: {entry.team_name}</p>
<p>ゼッケン番号: {entry.zekken_number}</p>
<p>クラス: {entry.class_name}</p>
"""
if hasattr(entry, 'start_info'):
team_info += f"<p>スタート時間: {entry.start_info.start_time.strftime('%Y-%m-%d %H:%M:%S')}</p>"
if hasattr(entry, 'goal_info'):
team_info += f"""
<p>ゴール時間: {entry.goal_info.goal_time.strftime('%Y-%m-%d %H:%M:%S')}</p>
<p>スコア: {entry.goal_info.score}点</p>
"""
team_info += "</div>"
# チーム情報をマップに追加
folium.Element(team_info).add_to(m)
# 一時的にHTMLファイルを保存
timestamp = timezone.now().strftime("%Y%m%d%H%M%S")
html_file = f"route_map_{event_code}_{zekken_number}_{timestamp}.html"
# 一時ディレクトリを作成
tmp_dir = os.path.join(settings.MEDIA_ROOT, 'tmp', 'route_maps')
os.makedirs(tmp_dir, exist_ok=True)
html_path = os.path.join(tmp_dir, html_file)
m.save(html_path)
# HTMLをPNGに変換
png_file = f"route_map_{event_code}_{zekken_number}_{timestamp}.png"
png_path = os.path.join(tmp_dir, png_file)
# Seleniumを使用してスクリーンショットを撮影
try:
# Chromeオプションを設定
options = webdriver.ChromeOptions()
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
# ウィンドウサイズを設定
options.add_argument('--window-size=1200,900')
# WebDriverを初期化
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
# HTMLファイルを開く
driver.get(f"file://{html_path}")
# 地図の読み込みを待機
time.sleep(2)
# スクリーンショットを撮影
driver.save_screenshot(png_path)
driver.quit()
# メディアディレクトリに移動
final_path = os.path.join('route_maps', png_file)
with open(png_path, 'rb') as f:
default_storage.save(final_path, ContentFile(f.read()))
# 画像のURLを生成
image_url = settings.MEDIA_URL + final_path
# 一時ファイルを削除
if os.path.exists(html_path):
os.remove(html_path)
if os.path.exists(png_path):
os.remove(png_path)
return Response({
"status": "OK",
"team_name": entry.team_name,
"event_code": event_code,
"zekken_number": zekken_number,
"image_url": image_url
})
except Exception as e:
logger.error(f"Error generating screenshot: {str(e)}")
return Response({
"status": "ERROR",
"message": "地図の画像生成に失敗しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
except Exception as e:
logger.error(f"Error in generate_route_image: {str(e)}")
return Response({
"status": "ERROR",
"message": "サーバーエラーが発生しました"
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)