981 lines
41 KiB
Python
Executable File
981 lines
41 KiB
Python
Executable File
# 既存のインポート部分に追加
|
||
from rest_framework.decorators import api_view
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rog.models import NewEvent2, Entry, Location2025, 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.ウェイポイントデータを保存するためのモデル Waypoint を定義
|
||
2.POST リクエストからチーム名、イベントコード、ウェイポイントデータ配列を取得
|
||
3.データのバリデーションを行う
|
||
- 必要なパラメータが全て揃っているか
|
||
- ウェイポイントデータが配列形式か
|
||
- 各ウェイポイントに必要な情報(緯度、経度、記録時間)が含まれているか
|
||
4.イベントとチームの存在確認
|
||
5.各ウェイポイントを処理
|
||
- 数値型・日時型の変換
|
||
- 重複チェック(同じ記録時間のデータは無視)
|
||
- データベースに保存
|
||
6.処理結果の統計(保存数、スキップ数、無効データ数)を含む応答を返す
|
||
|
||
この実装により、アプリからチームの位置情報を送信し、サーバーで保存することができます。
|
||
保存されたデータは、後でルート表示や分析などに使用できます。
|
||
"""
|
||
|
||
@api_view(['POST'])
|
||
def get_waypoint_datas_from_rogapp(request):
|
||
"""
|
||
アプリからウェイポイントデータを受信し保存
|
||
|
||
パラメータ:
|
||
- team_name: チーム名
|
||
- event_code: イベントコード
|
||
- waypoints: ウェイポイントデータの配列
|
||
"""
|
||
logger.info("get_waypoint_datas_from_rogapp called")
|
||
|
||
# リクエストからパラメータを取得
|
||
try:
|
||
data = request.data
|
||
team_name = data.get('team_name')
|
||
event_code = data.get('event_code')
|
||
waypoints_data = data.get('waypoints')
|
||
|
||
# JSONとして送信された場合はデコード
|
||
if isinstance(waypoints_data, str):
|
||
try:
|
||
waypoints_data = json.loads(waypoints_data)
|
||
except json.JSONDecodeError:
|
||
logger.warning("Invalid JSON format for waypoints")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "ウェイポイントデータのJSONフォーマットが無効です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
logger.debug(f"Parameters: team_name={team_name}, event_code={event_code}, "
|
||
f"waypoints_count={len(waypoints_data) if waypoints_data else 0}")
|
||
except Exception as e:
|
||
logger.warning(f"Error parsing request data: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "リクエストデータの解析に失敗しました"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# パラメータ検証
|
||
if not all([team_name, event_code, waypoints_data]):
|
||
logger.warning("Missing required parameters")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "チーム名、イベントコード、ウェイポイントデータが必要です"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
# ウェイポイントデータの検証
|
||
if not isinstance(waypoints_data, list):
|
||
logger.warning("Waypoints data is not a list")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "ウェイポイントデータはリスト形式である必要があります"
|
||
}, status=status.HTTP_400_BAD_REQUEST)
|
||
|
||
if len(waypoints_data) == 0:
|
||
logger.warning("Empty waypoints list")
|
||
return Response({
|
||
"status": "WARNING",
|
||
"message": "ウェイポイントリストが空です",
|
||
"saved_count": 0
|
||
})
|
||
|
||
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,
|
||
team_name=team_name
|
||
).first()
|
||
|
||
if not entry:
|
||
logger.warning(f"Team '{team_name}' not found in event: {event_code}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "指定されたチーム名のエントリーが見つかりません"
|
||
}, status=status.HTTP_404_NOT_FOUND)
|
||
|
||
# チームがスタートしているか確認(オプション)
|
||
if not hasattr(entry, 'start_info'):
|
||
logger.warning(f"Team {team_name} has not started yet")
|
||
# 必要に応じてエラーを返すか、自動的にスタート処理を行う
|
||
|
||
# トランザクション開始
|
||
saved_count = 0
|
||
skipped_count = 0
|
||
invalid_count = 0
|
||
|
||
with transaction.atomic():
|
||
for waypoint in waypoints_data:
|
||
try:
|
||
# 必須フィールドの検証
|
||
lat = waypoint.get('latitude')
|
||
lng = waypoint.get('longitude')
|
||
recorded_at_str = waypoint.get('recorded_at')
|
||
|
||
if not all([lat, lng, recorded_at_str]):
|
||
logger.warning(f"Missing required fields in waypoint: {waypoint}")
|
||
invalid_count += 1
|
||
continue
|
||
|
||
# 数値型の検証
|
||
try:
|
||
lat = float(lat)
|
||
lng = float(lng)
|
||
except (ValueError, TypeError):
|
||
logger.warning(f"Invalid coordinate values: lat={lat}, lng={lng}")
|
||
invalid_count += 1
|
||
continue
|
||
|
||
# 日時の解析
|
||
try:
|
||
# 複数の日付形式に対応
|
||
recorded_at = None
|
||
for date_format in [
|
||
"%Y-%m-%dT%H:%M:%S.%fZ", # ISO形式(ミリ秒付き)
|
||
"%Y-%m-%dT%H:%M:%SZ", # ISO形式
|
||
"%Y-%m-%d %H:%M:%S", # 標準形式
|
||
"%Y/%m/%d %H:%M:%S" # スラッシュ区切り
|
||
]:
|
||
try:
|
||
if recorded_at_str.endswith('Z'):
|
||
# UTCからローカルタイムに変換する必要がある場合
|
||
dt = datetime.strptime(recorded_at_str, date_format)
|
||
# ここでUTCからローカルタイムへの変換を行う場合
|
||
recorded_at = dt
|
||
else:
|
||
recorded_at = datetime.strptime(recorded_at_str, date_format)
|
||
break
|
||
except ValueError:
|
||
continue
|
||
|
||
if recorded_at is None:
|
||
raise ValueError(f"No matching date format for: {recorded_at_str}")
|
||
except ValueError as e:
|
||
logger.warning(f"Invalid date format: {recorded_at_str} - {str(e)}")
|
||
invalid_count += 1
|
||
continue
|
||
|
||
# 重複チェック(同じ記録時間のウェイポイントは無視)
|
||
if Waypoint.objects.filter(
|
||
entry=entry,
|
||
recorded_at=recorded_at
|
||
).exists():
|
||
logger.debug(f"Skipping duplicate waypoint at: {recorded_at_str}")
|
||
skipped_count += 1
|
||
continue
|
||
|
||
# オプションフィールドの取得
|
||
altitude = waypoint.get('altitude')
|
||
accuracy = waypoint.get('accuracy')
|
||
speed = waypoint.get('speed')
|
||
|
||
# ウェイポイントの保存
|
||
Waypoint.objects.create(
|
||
entry=entry,
|
||
latitude=lat,
|
||
longitude=lng,
|
||
altitude=float(altitude) if altitude not in [None, ''] else None,
|
||
accuracy=float(accuracy) if accuracy not in [None, ''] else None,
|
||
speed=float(speed) if speed not in [None, ''] else None,
|
||
recorded_at=recorded_at
|
||
)
|
||
|
||
saved_count += 1
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error processing waypoint: {str(e)}")
|
||
invalid_count += 1
|
||
|
||
logger.info(f"Processed waypoints for team {team_name}: "
|
||
f"saved={saved_count}, skipped={skipped_count}, invalid={invalid_count}")
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"message": "ウェイポイントデータを正常に保存しました",
|
||
"team_name": team_name,
|
||
"event_code": event_code,
|
||
"total_waypoints": len(waypoints_data),
|
||
"saved_count": saved_count,
|
||
"skipped_count": skipped_count,
|
||
"invalid_count": invalid_count
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in get_waypoint_datas_from_rogapp: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
"""
|
||
解説
|
||
この実装では以下の処理を行っています:
|
||
|
||
1.チーム名とイベントコードのパラメータを受け取ります
|
||
2.パラメータが不足している場合はエラーを返します
|
||
3.指定されたイベントとチームの存在を確認します
|
||
4.チームの移動経路データを取得します:
|
||
- ウェイポイントデータ(記録時間順)
|
||
- チェックポイント通過情報(時間順)
|
||
- スタート情報とゴール情報
|
||
5.移動経路の統計情報を計算します:
|
||
- 総移動距離
|
||
- ウェイポイント数
|
||
- チェックポイント数
|
||
6.GeoJSON形式のルートデータを作成します
|
||
7.全てのデータを含む応答を返します
|
||
|
||
実装のポイント:
|
||
|
||
- ウェイポイントとチェックポイントの両方をルートポイントとして統合
|
||
- チェックポイントの座標はLocationモデルから取得(存在する場合)
|
||
- ハバーサイン公式を使用して2点間の距離を計算し、総移動距離を算出
|
||
- GeoJSON形式のデータも含め、様々な用途に対応できる形式でデータを提供
|
||
|
||
このエンドポイントにより、チームの移動経路を視覚化したり分析したりすることができます。
|
||
"""
|
||
|
||
@api_view(['GET'])
|
||
def get_route(request):
|
||
"""
|
||
指定チームのルート情報を取得
|
||
|
||
パラメータ:
|
||
- team: チーム名
|
||
- event_code: イベントコード
|
||
"""
|
||
logger.info("get_route called")
|
||
|
||
# リクエストからパラメータを取得
|
||
team_name = request.query_params.get('team')
|
||
event_code = request.query_params.get('event_code')
|
||
|
||
logger.debug(f"Parameters: team={team_name}, event_code={event_code}")
|
||
|
||
# パラメータ検証
|
||
if not all([team_name, event_code]):
|
||
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,
|
||
team_name=team_name
|
||
).first()
|
||
|
||
if not entry:
|
||
logger.warning(f"Team '{team_name}' 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')
|
||
|
||
# チェックポイント通過情報を取得(時間順)
|
||
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,
|
||
"image_url": cp.image_address,
|
||
"is_service_checked": getattr(cp, 'is_service_checked', False)
|
||
}
|
||
|
||
# チェックポイントの座標情報を取得(Location2025モデルがある場合)
|
||
try:
|
||
event_cp = Location2025.objects.filter(
|
||
event_id=event.id,
|
||
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
|
||
cp_data["is_service_cp"] = event_cp.is_service_cp
|
||
|
||
# ルートポイントとしても追加
|
||
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,
|
||
"cp_name": event_cp.cp_name
|
||
})
|
||
except:
|
||
# Locationモデルが存在しないか、座標が設定されていない場合
|
||
pass
|
||
|
||
checkpoint_data.append(cp_data)
|
||
|
||
# ルートの統計情報を計算
|
||
total_distance = 0
|
||
if len(route_points) >= 2:
|
||
from math import sin, cos, sqrt, atan2, radians
|
||
|
||
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
|
||
|
||
# GeoJSON形式のルートデータを作成(オプション)
|
||
geo_json = {
|
||
"type": "FeatureCollection",
|
||
"features": [
|
||
{
|
||
"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": team_name,
|
||
"total_distance": round(total_distance, 2), # km単位で小数点以下2桁まで
|
||
"waypoint_count": waypoints.count(),
|
||
"checkpoint_count": checkpoints.count()
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"team_info": {
|
||
"team_name": team_name,
|
||
"zekken_number": entry.zekken_number,
|
||
"class_name": entry.class_name,
|
||
"event_code": event_code
|
||
},
|
||
"start_info": start_info,
|
||
"goal_info": goal_info,
|
||
"route_statistics": {
|
||
"total_distance": round(total_distance, 2), # km単位
|
||
"total_waypoints": waypoints.count(),
|
||
"total_checkpoints": checkpoints.count()
|
||
},
|
||
"checkpoints": checkpoint_data,
|
||
"route_points": route_points,
|
||
"geo_json": geo_json
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in get_route: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
"""
|
||
解説
|
||
この実装では以下の処理を行っています:
|
||
|
||
1.ゼッケン番号とイベントコードのパラメータを受け取ります
|
||
2.オプションで limit(返す位置情報の最大数)と time_range(時間範囲)のパラメータも受け取ります
|
||
3.パラメータが不足している場合はエラーを返します
|
||
4.指定されたイベントとチームの存在を確認します
|
||
5.チームのウェイポイントデータを取得します:
|
||
- 新しい順(降順)にソート
|
||
- 時間範囲でフィルタリング(指定された場合)
|
||
- 件数制限(指定された場合)
|
||
6.チームの基本情報、最新の位置情報、位置情報履歴を含む応答を作成します
|
||
7.GeoJSON形式のデータも提供します:
|
||
- 移動経路を表す LineString フィーチャー
|
||
- 各位置を表す Point フィーチャー
|
||
|
||
この実装は /getRoute エンドポイントに似ていますが、以下の点で異なります:
|
||
|
||
- 位置情報の取得に焦点を当てています(チェックポイント情報は含まれません)
|
||
- より新しいデータを優先的に返します(降順ソート)
|
||
- 時間範囲や件数制限のオプションがあります
|
||
- 移動経路の統計情報(距離など)は計算しません
|
||
- このエンドポイントは、リアルタイムマップ表示やチームの動きを追跡する場合に便利です。
|
||
"""
|
||
|
||
# 既存のインポート部分に追加
|
||
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
|
||
import logging
|
||
from django.db.models import F, Q
|
||
from datetime import datetime, timedelta
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
@api_view(['GET'])
|
||
def fetch_user_locations(request):
|
||
"""
|
||
ユーザーの位置情報履歴を取得
|
||
|
||
パラメータ:
|
||
- zekken_number: ゼッケン番号
|
||
- event_code: イベントコード
|
||
"""
|
||
logger.info("fetch_user_locations called")
|
||
|
||
# リクエストからパラメータを取得
|
||
zekken_number = request.query_params.get('zekken_number')
|
||
event_code = request.query_params.get('event_code')
|
||
|
||
# 追加のオプションパラメータ
|
||
limit = request.query_params.get('limit')
|
||
if limit:
|
||
try:
|
||
limit = int(limit)
|
||
except ValueError:
|
||
limit = None
|
||
|
||
time_range = request.query_params.get('time_range') # 例: "1h", "30m", "2d"
|
||
|
||
logger.debug(f"Parameters: zekken_number={zekken_number}, event_code={event_code}, "
|
||
f"limit={limit}, time_range={time_range}")
|
||
|
||
# パラメータ検証
|
||
if not all([zekken_number, event_code]):
|
||
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_query = Waypoint.objects.filter(entry=entry)
|
||
|
||
# 時間範囲でフィルタリング(オプション)
|
||
if time_range:
|
||
# 時間範囲をパース (例: "1h", "30m", "2d")
|
||
time_value = ""
|
||
time_unit = ""
|
||
for char in time_range:
|
||
if char.isdigit():
|
||
time_value += char
|
||
else:
|
||
time_unit += char
|
||
|
||
if time_value and time_unit:
|
||
time_value = int(time_value)
|
||
now = datetime.now()
|
||
|
||
if time_unit.lower() == 'h':
|
||
start_time = now - timedelta(hours=time_value)
|
||
elif time_unit.lower() == 'm':
|
||
start_time = now - timedelta(minutes=time_value)
|
||
elif time_unit.lower() == 'd':
|
||
start_time = now - timedelta(days=time_value)
|
||
else:
|
||
# デフォルトは1時間
|
||
start_time = now - timedelta(hours=1)
|
||
|
||
waypoints_query = waypoints_query.filter(recorded_at__gte=start_time)
|
||
|
||
# 時間順にソート
|
||
waypoints_query = waypoints_query.order_by('-recorded_at')
|
||
|
||
# 件数制限(オプション)
|
||
if limit:
|
||
waypoints_query = waypoints_query[:limit]
|
||
|
||
# データの取得
|
||
waypoints = list(waypoints_query)
|
||
|
||
# チームのスタート・ゴール情報
|
||
start_time = None
|
||
if hasattr(entry, 'start_info'):
|
||
start_time = entry.start_info.start_time
|
||
|
||
goal_time = None
|
||
if hasattr(entry, 'goal_info'):
|
||
goal_time = entry.goal_info.goal_time
|
||
|
||
# レスポンスデータの構築
|
||
location_data = []
|
||
|
||
for wp in waypoints:
|
||
location = {
|
||
"latitude": wp.latitude,
|
||
"longitude": wp.longitude,
|
||
"timestamp": wp.recorded_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||
}
|
||
|
||
# オプションフィールドがあれば追加
|
||
if wp.altitude is not None:
|
||
location["altitude"] = wp.altitude
|
||
if wp.accuracy is not None:
|
||
location["accuracy"] = wp.accuracy
|
||
if wp.speed is not None:
|
||
location["speed"] = wp.speed
|
||
|
||
location_data.append(location)
|
||
|
||
# 最後の位置情報
|
||
latest_location = None
|
||
if location_data:
|
||
latest_location = location_data[0] # すでに新しい順にソート済み
|
||
|
||
# GeoJSON形式のデータも作成
|
||
geo_json = {
|
||
"type": "FeatureCollection",
|
||
"features": [
|
||
{
|
||
"type": "Feature",
|
||
"geometry": {
|
||
"type": "LineString",
|
||
"coordinates": [
|
||
[point["longitude"], point["latitude"]]
|
||
for point in reversed(location_data) # 時系列順に並べ直す
|
||
]
|
||
},
|
||
"properties": {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": zekken_number,
|
||
"point_count": len(location_data)
|
||
}
|
||
}
|
||
]
|
||
}
|
||
|
||
# ポイントを個別のフィーチャーとしても追加
|
||
for point in location_data:
|
||
geo_json["features"].append({
|
||
"type": "Feature",
|
||
"geometry": {
|
||
"type": "Point",
|
||
"coordinates": [point["longitude"], point["latitude"]]
|
||
},
|
||
"properties": {
|
||
"timestamp": point["timestamp"],
|
||
"speed": point.get("speed"),
|
||
"altitude": point.get("altitude")
|
||
}
|
||
})
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"team_info": {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": zekken_number,
|
||
"class_name": entry.class_name,
|
||
"event_code": event_code,
|
||
"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
|
||
},
|
||
"latest_location": latest_location,
|
||
"locations": location_data,
|
||
"total_locations": len(location_data),
|
||
"geo_json": geo_json
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in fetch_user_locations: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|
||
|
||
"""
|
||
解説
|
||
この実装では以下の処理を行っています:
|
||
|
||
1.イベントコードとオプションでクラス名のパラメータを受け取ります
|
||
2.パラメータが不足している場合はエラーを返します
|
||
3.指定されたイベントの存在を確認します
|
||
4.イベント内のチームをクエリします:
|
||
- スタート情報があるチームのみを対象
|
||
- クラス名が指定された場合はそのクラスに所属するチームのみに限定
|
||
5.各チームについて以下の情報を取得します:
|
||
- チームの基本情報(名前、ゼッケン番号、クラス)
|
||
- スタート情報とゴール情報
|
||
- ウェイポイントとチェックポイントのデータ
|
||
- ルートの統計情報(総距離、ウェイポイント数、チェックポイント数)
|
||
6.全チームの情報と、GeoJSON形式のデータを含む応答を返します
|
||
|
||
実装のポイント:
|
||
|
||
- レスポンスサイズを抑えるため、各チームのルートポイント詳細は含めず、統計情報のみを提供
|
||
- GeoJSON形式のデータでは、各チームのルートを1つのフィーチャーとして表現
|
||
- 各チームのデータ処理中にエラーが発生した場合でも、他のチームの処理は継続する例外処理
|
||
- オプションでクラス名によるフィルタリングが可能
|
||
|
||
このエンドポイントは、管理画面での全チームの一覧表示や、全チームのルートを1つの地図上に表示したい場合に役立ちます。
|
||
"""
|
||
|
||
@api_view(['GET'])
|
||
def get_all_routes(request):
|
||
"""
|
||
指定イベントの全チームのルート情報を取得
|
||
|
||
パラメータ:
|
||
- event_code: イベントコード
|
||
- class_name: クラス名(省略可)
|
||
"""
|
||
logger.info("get_all_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')
|
||
|
||
logger.debug(f"Parameters: event_code={event_code}, class_name={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)
|
||
if class_name:
|
||
entries_query = entries_query.filter(class_name=class_name)
|
||
|
||
# スタート情報があるチームだけ対象にする
|
||
entries_with_start = entries_query.filter(start_info__isnull=False)
|
||
|
||
if not entries_with_start.exists():
|
||
logger.warning(f"No teams with start info found for event: {event_code}")
|
||
return Response({
|
||
"status": "WARNING",
|
||
"message": "スタート済みのチームが見つかりません",
|
||
"teams": [],
|
||
"total_teams": 0
|
||
})
|
||
|
||
# 全チームの情報とルートを取得
|
||
teams_data = []
|
||
geo_json_features = []
|
||
|
||
for entry in entries_with_start:
|
||
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 = Location2025.objects.filter(
|
||
event_id=event.id,
|
||
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 = {
|
||
"team_name": entry.team_name,
|
||
"zekken_number": entry.zekken_number,
|
||
"class_name": entry.class_name,
|
||
"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,
|
||
# ルートポイントは量が多いため、必要に応じて個別ルート取得APIを使用する形に
|
||
"route_points_count": len(route_points)
|
||
}
|
||
|
||
teams_data.append(team_data)
|
||
|
||
# GeoJSONフィーチャーを追加
|
||
if route_points:
|
||
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,
|
||
"total_distance": round(total_distance, 2),
|
||
"waypoint_count": waypoints.count(),
|
||
"checkpoint_count": checkpoints.count(),
|
||
"score": goal_info["score"] if goal_info else None
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error processing route for team {entry.team_name}: {str(e)}")
|
||
# エラーが発生したチームはスキップ
|
||
continue
|
||
|
||
# GeoJSON形式のデータを作成
|
||
geo_json = {
|
||
"type": "FeatureCollection",
|
||
"features": geo_json_features
|
||
}
|
||
|
||
return Response({
|
||
"status": "OK",
|
||
"event_code": event_code,
|
||
"class_name": class_name,
|
||
"total_teams": len(teams_data),
|
||
"teams": teams_data,
|
||
"geo_json": geo_json
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error in get_all_routes: {str(e)}")
|
||
return Response({
|
||
"status": "ERROR",
|
||
"message": "サーバーエラーが発生しました"
|
||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||
|