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