1313
1414class 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