Merge pull request 'release: 2026-03-24.3 (어구그룹 탐지 수정)' (#181) from develop into main
All checks were successful
Deploy KCG / deploy (push) Successful in 1m48s
All checks were successful
Deploy KCG / deploy (push) Successful in 1m48s
This commit is contained in:
커밋
89786f1ec3
@ -108,17 +108,16 @@ def build_group_polygon(
|
||||
|
||||
|
||||
def detect_gear_groups(
|
||||
vessel_dfs: dict,
|
||||
vessel_store,
|
||||
now: Optional[datetime] = None,
|
||||
) -> list[dict]:
|
||||
"""어구 이름 패턴으로 어구그룹을 탐지한다.
|
||||
|
||||
프론트엔드 FleetClusterLayer.tsx gearGroupMap useMemo 로직 이관.
|
||||
전체 AIS 선박(vessel_store._tracks)에서 어구 패턴을 탐지한다.
|
||||
|
||||
Args:
|
||||
vessel_dfs: {mmsi: DataFrame} — 각 DataFrame은 lat, lon, sog, cog, timestamp 칼럼.
|
||||
vessel_store: VesselStore — get_vessel_info(mmsi) → {name, ...}.
|
||||
vessel_store: VesselStore — get_all_latest_positions() + get_vessel_info().
|
||||
now: 기준 시각 (None이면 UTC now).
|
||||
|
||||
Returns:
|
||||
@ -127,30 +126,29 @@ def detect_gear_groups(
|
||||
if now is None:
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
# 선박명 → mmsi 맵 (모선 탐색용)
|
||||
# 전체 선박의 최신 위치 가져오기
|
||||
all_positions = vessel_store.get_all_latest_positions()
|
||||
|
||||
# 선박명 → mmsi 맵 (모선 탐색용, 어구 패턴이 아닌 선박만)
|
||||
name_to_mmsi: dict[str, str] = {}
|
||||
for mmsi, df in vessel_dfs.items():
|
||||
if df is None or len(df) == 0:
|
||||
continue
|
||||
info = vessel_store.get_vessel_info(mmsi)
|
||||
name: str = (info or {}).get('name', '') or ''
|
||||
name = name.strip()
|
||||
for mmsi, pos in all_positions.items():
|
||||
name = (pos.get('name') or '').strip()
|
||||
if name and not GEAR_PATTERN.match(name):
|
||||
name_to_mmsi[name] = mmsi
|
||||
|
||||
# 1단계: 같은 모선명 어구 수집 (60분 이내만)
|
||||
raw_groups: dict[str, list[dict]] = {}
|
||||
for mmsi, df in vessel_dfs.items():
|
||||
if df is None or len(df) == 0:
|
||||
for mmsi, pos in all_positions.items():
|
||||
name = (pos.get('name') or '').strip()
|
||||
if not name:
|
||||
continue
|
||||
last = df.iloc[-1]
|
||||
ts = last.get('timestamp') if hasattr(last, 'get') else last['timestamp']
|
||||
|
||||
# timestamp → datetime 변환
|
||||
# staleness 체크
|
||||
ts = pos.get('timestamp')
|
||||
if ts is not None:
|
||||
if isinstance(ts, datetime):
|
||||
last_dt = ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc)
|
||||
else:
|
||||
# pandas Timestamp 또는 숫자(unix seconds)
|
||||
try:
|
||||
import pandas as pd
|
||||
last_dt = pd.Timestamp(ts).to_pydatetime()
|
||||
@ -158,15 +156,10 @@ def detect_gear_groups(
|
||||
last_dt = last_dt.replace(tzinfo=timezone.utc)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
age_sec = (now - last_dt).total_seconds()
|
||||
if age_sec > STALE_SEC:
|
||||
continue
|
||||
|
||||
info = vessel_store.get_vessel_info(mmsi)
|
||||
name = (info or {}).get('name', '') or ''
|
||||
name = name.strip()
|
||||
|
||||
m = GEAR_PATTERN.match(name)
|
||||
if not m:
|
||||
continue
|
||||
@ -175,10 +168,10 @@ def detect_gear_groups(
|
||||
entry = {
|
||||
'mmsi': mmsi,
|
||||
'name': name,
|
||||
'lat': float(last['lat']),
|
||||
'lon': float(last['lon']),
|
||||
'sog': float(last.get('sog', 0) if hasattr(last, 'get') else last['sog']),
|
||||
'cog': float(last.get('cog', 0) if hasattr(last, 'get') else last['cog']),
|
||||
'lat': pos['lat'],
|
||||
'lon': pos['lon'],
|
||||
'sog': pos.get('sog', 0),
|
||||
'cog': pos.get('cog', 0),
|
||||
}
|
||||
raw_groups.setdefault(parent_name, []).append(entry)
|
||||
|
||||
@ -191,12 +184,10 @@ def detect_gear_groups(
|
||||
anchor_lat: Optional[float] = None
|
||||
anchor_lon: Optional[float] = None
|
||||
|
||||
if parent_mmsi and parent_mmsi in vessel_dfs:
|
||||
parent_df = vessel_dfs[parent_mmsi]
|
||||
if parent_df is not None and len(parent_df) > 0:
|
||||
parent_last = parent_df.iloc[-1]
|
||||
anchor_lat = float(parent_last['lat'])
|
||||
anchor_lon = float(parent_last['lon'])
|
||||
if parent_mmsi and parent_mmsi in all_positions:
|
||||
parent_pos = all_positions[parent_mmsi]
|
||||
anchor_lat = parent_pos['lat']
|
||||
anchor_lon = parent_pos['lon']
|
||||
|
||||
if anchor_lat is None and gears:
|
||||
anchor_lat = gears[0]['lat']
|
||||
@ -240,7 +231,6 @@ def detect_gear_groups(
|
||||
|
||||
|
||||
def build_all_group_snapshots(
|
||||
vessel_dfs: dict,
|
||||
vessel_store,
|
||||
company_vessels: dict[int, list[str]],
|
||||
companies: dict[int, dict],
|
||||
@ -250,8 +240,7 @@ def build_all_group_snapshots(
|
||||
Shapely 미설치 시 빈 리스트를 반환한다.
|
||||
|
||||
Args:
|
||||
vessel_dfs: {mmsi: DataFrame}.
|
||||
vessel_store: VesselStore — get_vessel_info(mmsi).
|
||||
vessel_store: VesselStore — get_all_latest_positions() + get_vessel_info().
|
||||
company_vessels: {company_id: [mmsi_list]}.
|
||||
companies: {id: {name_cn, name_en}}.
|
||||
|
||||
@ -264,6 +253,7 @@ def build_all_group_snapshots(
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
snapshots: list[dict] = []
|
||||
all_positions = vessel_store.get_all_latest_positions()
|
||||
|
||||
# ── FLEET 타입: company_vessels 순회 ──────────────────────────
|
||||
for company_id, mmsi_list in company_vessels.items():
|
||||
@ -275,20 +265,17 @@ def build_all_group_snapshots(
|
||||
members: list[dict] = []
|
||||
|
||||
for mmsi in mmsi_list:
|
||||
df = vessel_dfs.get(mmsi)
|
||||
if df is None or len(df) == 0:
|
||||
pos = all_positions.get(mmsi)
|
||||
if not pos:
|
||||
continue
|
||||
last = df.iloc[-1]
|
||||
lat = float(last['lat'])
|
||||
lon = float(last['lon'])
|
||||
sog_val = last.get('sog', 0) if hasattr(last, 'get') else last['sog']
|
||||
cog_val = last.get('cog', 0) if hasattr(last, 'get') else last['cog']
|
||||
sog = float(sog_val)
|
||||
cog = float(cog_val)
|
||||
lat = pos['lat']
|
||||
lon = pos['lon']
|
||||
sog = pos.get('sog', 0)
|
||||
cog = pos.get('cog', 0)
|
||||
points.append((lon, lat))
|
||||
members.append({
|
||||
'mmsi': mmsi,
|
||||
'name': (vessel_store.get_vessel_info(mmsi) or {}).get('name', ''),
|
||||
'name': pos.get('name', ''),
|
||||
'lat': lat,
|
||||
'lon': lon,
|
||||
'sog': sog,
|
||||
@ -321,7 +308,7 @@ def build_all_group_snapshots(
|
||||
})
|
||||
|
||||
# ── GEAR 타입: detect_gear_groups 결과 순회 ───────────────────
|
||||
gear_groups = detect_gear_groups(vessel_dfs, vessel_store, now=now)
|
||||
gear_groups = detect_gear_groups(vessel_store, now=now)
|
||||
|
||||
for group in gear_groups:
|
||||
parent_name: str = group['parent_name']
|
||||
@ -332,12 +319,10 @@ def build_all_group_snapshots(
|
||||
anchor_lat: Optional[float] = None
|
||||
anchor_lon: Optional[float] = None
|
||||
|
||||
if parent_mmsi and parent_mmsi in vessel_dfs:
|
||||
parent_df = vessel_dfs.get(parent_mmsi)
|
||||
if parent_df is not None and len(parent_df) > 0:
|
||||
p_last = parent_df.iloc[-1]
|
||||
anchor_lat = float(p_last['lat'])
|
||||
anchor_lon = float(p_last['lon'])
|
||||
if parent_mmsi and parent_mmsi in all_positions:
|
||||
parent_pos = all_positions[parent_mmsi]
|
||||
anchor_lat = parent_pos['lat']
|
||||
anchor_lon = parent_pos['lon']
|
||||
|
||||
if anchor_lat is None and gear_members:
|
||||
anchor_lat = gear_members[0]['lat']
|
||||
@ -357,12 +342,9 @@ def build_all_group_snapshots(
|
||||
|
||||
# 폴리곤 points: 어구 좌표 + 모선 좌표
|
||||
points = [(g['lon'], g['lat']) for g in gear_members]
|
||||
if parent_mmsi and parent_mmsi in vessel_dfs:
|
||||
parent_df = vessel_dfs.get(parent_mmsi)
|
||||
if parent_df is not None and len(parent_df) > 0:
|
||||
p_last = parent_df.iloc[-1]
|
||||
p_lat = float(p_last['lat'])
|
||||
p_lon = float(p_last['lon'])
|
||||
if parent_mmsi and parent_mmsi in all_positions:
|
||||
parent_pos = all_positions[parent_mmsi]
|
||||
p_lon, p_lat = parent_pos['lon'], parent_pos['lat']
|
||||
if (p_lon, p_lat) not in points:
|
||||
points.append((p_lon, p_lat))
|
||||
|
||||
@ -373,19 +355,15 @@ def build_all_group_snapshots(
|
||||
# members JSONB 구성
|
||||
members_out: list[dict] = []
|
||||
# 모선 먼저
|
||||
if parent_mmsi and parent_mmsi in vessel_dfs:
|
||||
parent_df = vessel_dfs.get(parent_mmsi)
|
||||
if parent_df is not None and len(parent_df) > 0:
|
||||
p_last = parent_df.iloc[-1]
|
||||
p_sog = float(p_last.get('sog', 0) if hasattr(p_last, 'get') else p_last['sog'])
|
||||
p_cog = float(p_last.get('cog', 0) if hasattr(p_last, 'get') else p_last['cog'])
|
||||
if parent_mmsi and parent_mmsi in all_positions:
|
||||
parent_pos = all_positions[parent_mmsi]
|
||||
members_out.append({
|
||||
'mmsi': parent_mmsi,
|
||||
'name': parent_name,
|
||||
'lat': float(p_last['lat']),
|
||||
'lon': float(p_last['lon']),
|
||||
'sog': p_sog,
|
||||
'cog': p_cog,
|
||||
'lat': parent_pos['lat'],
|
||||
'lon': parent_pos['lon'],
|
||||
'sog': parent_pos.get('sog', 0),
|
||||
'cog': parent_pos.get('cog', 0),
|
||||
'role': 'PARENT',
|
||||
'isParent': True,
|
||||
})
|
||||
|
||||
18
prediction/cache/vessel_store.py
vendored
18
prediction/cache/vessel_store.py
vendored
@ -317,6 +317,24 @@ class VesselStore:
|
||||
"""Return static vessel info dict for the given MMSI, or empty dict if not found."""
|
||||
return self._static_info.get(mmsi, {})
|
||||
|
||||
def get_all_latest_positions(self) -> dict[str, dict]:
|
||||
"""모든 선박의 최신 위치 반환. {mmsi: {lat, lon, sog, cog, timestamp, name}}"""
|
||||
result: dict[str, dict] = {}
|
||||
for mmsi, df in self._tracks.items():
|
||||
if df is None or len(df) == 0:
|
||||
continue
|
||||
last = df.iloc[-1]
|
||||
info = self._static_info.get(mmsi, {})
|
||||
result[mmsi] = {
|
||||
'lat': float(last['lat']),
|
||||
'lon': float(last['lon']),
|
||||
'sog': float(last.get('sog', 0) or 0),
|
||||
'cog': float(last.get('cog', 0) or 0),
|
||||
'timestamp': last.get('timestamp'),
|
||||
'name': info.get('name', ''),
|
||||
}
|
||||
return result
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Properties
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@ -104,9 +104,9 @@ def run_analysis_cycle():
|
||||
from algorithms.polygon_builder import detect_gear_groups, build_all_group_snapshots
|
||||
|
||||
company_vessels = fleet_tracker.get_company_vessels(vessel_dfs)
|
||||
gear_groups = detect_gear_groups(vessel_dfs, vessel_store)
|
||||
gear_groups = detect_gear_groups(vessel_store)
|
||||
group_snapshots = build_all_group_snapshots(
|
||||
vessel_dfs, vessel_store, company_vessels,
|
||||
vessel_store, company_vessels,
|
||||
fleet_tracker._companies,
|
||||
)
|
||||
saved = kcgdb.save_group_snapshots(group_snapshots)
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user