Skip to content

Commit 11a8914

Browse files
committed
Track ports of interest, log infractions if transiting
1 parent 5d40541 commit 11a8914

11 files changed

Lines changed: 623 additions & 15 deletions

File tree

app/Http/Controllers/Api/VesselController.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,9 +593,25 @@ public function activities(string $mmsi): JsonResponse
593593
->orderBy('started_at', 'desc')
594594
->get();
595595

596+
$data = $activities->map(function ($activity) {
597+
$details = $activity->details;
598+
$source = $details['source'] ?? 'FleetLeaks';
599+
600+
return [
601+
'id' => $activity->id,
602+
'type' => $activity->type,
603+
'severity' => $activity->severity,
604+
'description' => $activity->description,
605+
'details' => $details,
606+
'source' => $source,
607+
'started_at' => $activity->started_at,
608+
'ended_at' => $activity->ended_at,
609+
];
610+
});
611+
596612
return response()->json([
597613
'mmsi' => (int) $mmsi,
598-
'data' => $activities,
614+
'data' => $data,
599615
]);
600616
}
601617

@@ -711,6 +727,7 @@ public function infractionsList(Request $request): JsonResponse
711727
'infractions_count' => (int) $vessel->activities_count,
712728
'highest_severity' => $highestSeverity,
713729
'risk_score' => min(100, (int) $vessel->raw_score),
730+
'source' => 'FleetLeaks',
714731
];
715732
});
716733

@@ -724,4 +741,34 @@ public function infractionsList(Request $request): JsonResponse
724741
],
725742
]);
726743
}
744+
745+
/**
746+
* List Ports of Interest
747+
*
748+
* Retrieve the list of high-risk maritime hubs and ports being monitored by SIST.
749+
*
750+
* @response 200 scenario="Success" {
751+
* "data": [
752+
* {
753+
* "name": "CPC Marine Terminal",
754+
* "lat": 44.6300,
755+
* "lng": 37.6400,
756+
* "severity": "high",
757+
* "type": "Major export hub",
758+
* "source": "FleetLeaks"
759+
* }
760+
* ]
761+
* }
762+
*/
763+
public function portsOfInterest(): JsonResponse
764+
{
765+
$path = resource_path('data/ports_of_interest.json');
766+
if (! file_exists($path)) {
767+
return response()->json(['data' => []]);
768+
}
769+
770+
$zones = json_decode(file_get_contents($path), true);
771+
772+
return response()->json(['data' => $zones]);
773+
}
727774
}

app/Services/VesselAnalysisService.php

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
class VesselAnalysisService
1515
{
16+
/** @var array|null */
17+
private $monitoredZones = null;
18+
1619
/**
1720
* Perform behavioral analysis on vessels with pending updates.
1821
*
@@ -50,8 +53,9 @@ public function rebuildAll(): void
5053
Vessel::query()->update(['last_analyzed_at' => null]);
5154
});
5255

53-
Vessel::chunk(100, function ($vessels) {
56+
Vessel::query()->chunk(100, function ($vessels) {
5457
foreach ($vessels as $vessel) {
58+
/** @var Vessel $vessel */
5559
$this->processVesselMetrics($vessel);
5660
}
5761
});
@@ -69,6 +73,7 @@ public function processVesselMetrics(Vessel $vessel): void
6973
$this->detectTransmissionGaps($vessel);
7074
$this->detectLoiteringPatterns($vessel);
7175
$this->detectKinematicAnomalies($vessel);
76+
$this->detectPortInteractions($vessel);
7277

7378
$vessel->update(['last_analyzed_at' => now()]);
7479
DB::commit();
@@ -201,6 +206,60 @@ private function detectKinematicAnomalies(Vessel $vessel): void
201206
}
202207
}
203208

209+
/**
210+
* Detects interactions with monitored ports of interest.
211+
*/
212+
private function detectPortInteractions(Vessel $vessel): void
213+
{
214+
if ($this->monitoredZones === null) {
215+
$path = resource_path('data/ports_of_interest.json');
216+
if (file_exists($path)) {
217+
$this->monitoredZones = json_decode(file_get_contents($path), true);
218+
} else {
219+
$this->monitoredZones = [];
220+
}
221+
}
222+
223+
if (empty($this->monitoredZones)) {
224+
return;
225+
}
226+
227+
$positions = $vessel->positions()
228+
->where('recorded_at', '>=', now()->subDays(30))
229+
->orderBy('recorded_at', 'desc')
230+
->get();
231+
232+
if ($positions->isEmpty()) {
233+
return;
234+
}
235+
236+
foreach ($this->monitoredZones as $zone) {
237+
$matchingPositions = $positions->filter(function ($pos) use ($zone) {
238+
// Radius of 2km for high accuracy (visit detection)
239+
return $this->calculateDistance($pos->lat, $pos->lng, $zone['lat'], $zone['lng']) <= 2.0;
240+
});
241+
242+
// If we have 3 or more points within the radius, it's a high-confidence visit/interaction
243+
if ($matchingPositions->count() >= 3) {
244+
$start = $matchingPositions->min('recorded_at');
245+
$end = $matchingPositions->max('recorded_at');
246+
$durationMinutes = $start->diffInMinutes($end);
247+
248+
// Only flag if they stayed for at least 30 minutes to filter out slow bypasses
249+
if ($durationMinutes >= 30) {
250+
$this->persistActivity($vessel, 'port_of_interest', $zone['severity'], [
251+
'port_name' => $zone['name'],
252+
'port_type' => $zone['type'],
253+
'source' => $zone['source'] ?? 'Unknown',
254+
'visit_duration_minutes' => $durationMinutes,
255+
'point_count' => $matchingPositions->count(),
256+
'coordinates' => ['lat' => $zone['lat'], 'lng' => $zone['lng']],
257+
], $start, $end);
258+
}
259+
}
260+
}
261+
}
262+
204263
/**
205264
* Logs or updates a detected behavioral event.
206265
*/
@@ -251,6 +310,7 @@ private function resolveDescription(string $type, array $details): string
251310
'ais_gap' => 'AIS transmission interruption detected ('.MaritimeFormatter::formatDuration($details['duration_minutes'] ?? 0).').',
252311
'loitering' => 'Stationary residency pattern in open-sea transit area.',
253312
'speed_anomaly' => 'Kinematic violation: speed exceeds physical capability ('.round($details['reported_speed'] ?? 0, 1).' kn).',
313+
'port_of_interest' => 'Vessel interaction detected at high-risk maritime hub: '.($details['port_name'] ?? 'Unknown Port').'.',
254314
default => 'Anomalous behavioral event detected.',
255315
};
256316
}

0 commit comments

Comments
 (0)