package com.grigowashere.aismap.controllers; import android.content.Context; import android.util.Log; import com.grigowashere.aismap.models.Vessel; import com.grigowashere.aismap.models.AISVessel; import com.grigowashere.aismap.maps.MapInterface; import com.grigowashere.aismap.data.Repository; import com.grigowashere.aismap.data.mapper.AISVesselMapper; import com.grigowashere.aismap.services.NotificationService; import com.grigowashere.aismap.utils.SettingsManager; import com.grigowashere.aismap.ui.UIDataChangeNotifier; import java.util.List; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Главный контроллер приложения * Координирует работу всех компонентов * Использует гибридный подход: координаты через Location API, остальное через NMEA */ public class AppController implements NMEAParser.NMEAParserListener, UDPListener.UDPListenerCallback, AndroidNMEAListener.NMEAMessageCallback, GPSLocationListener.LocationCallback, MapInterface.MarkerClickListener { private static final String TAG = "AppController"; private Context context; private NMEAParser nmeaParser; private UDPListener udpListener; private AndroidNMEAListener androidNmeaListener; private GPSLocationListener gpsLocationListener; private MapInterface mapInterface; private Vessel ownVessel; private List aisVessels; private ExecutorService executor; private com.grigowashere.aismap.data.Repository repository; private NotificationService notificationService; private SettingsManager settingsManager; private VesselPathController pathController; // VesselPathController для каждого AIS судна (ключ: MMSI) private final Map aisPathControllers = new HashMap<>(); private boolean isUDPEnabled; private boolean isAndroidNMEAEnabled; private boolean isUDPNMEAEnabled; private boolean isGPSLocationEnabled; private int udpPort; private String dataMode; // Время последнего получения сообщений ($ GPS) и (! AIS) в elapsedRealtime private long lastGPSMessageRealtimeMs; private long lastAISMessageRealtimeMs; // Периодическая очистка БД от устаревших AIS целей private android.os.Handler dbCleanupHandler; private Runnable dbCleanupRunnable; private static final long DB_CLEANUP_INTERVAL = 60000; // 1 минута // Единый Handler для всех UI операций (предотвращение утечек Handler'ов) private android.os.Handler uiHandler; // Индикаторы UI данных для централизованного throttling private UIDataChangeNotifier uiDataNotifier; // Callback для обновления UI (legacy для MainActivity) private UIUpdateCallback uiUpdateCallback; // Диагностика сервисов private long lastServiceLogTime = 0; public interface UIUpdateCallback { void onVesselPositionUpdated(Vessel vessel); void onGPSQualityUpdated(Vessel vessel); } /** * Расширенный интерфейс для дополнительных UI событий */ public interface ExtendedUIUpdateCallback extends UIUpdateCallback { void onShowOwnVesselBottomSheet(); void onShowAISVesselInfo(AISVessel vessel); void onUpdateCompass(float azimuth, List nearbyVessels); } public AppController(Context context) { this.context = context; this.ownVessel = new Vessel(); this.aisVessels = new ArrayList<>(); this.executor = Executors.newCachedThreadPool(); this.repository = new com.grigowashere.aismap.data.Repository(context); this.notificationService = new NotificationService(context); this.settingsManager = new SettingsManager(context); this.pathController = new VesselPathController(context, settingsManager); // Инициализируем Handler для периодической очистки БД this.dbCleanupHandler = new android.os.Handler(android.os.Looper.getMainLooper()); this.dbCleanupRunnable = this::performDatabaseCleanup; // Инициализируем единый UI Handler this.uiHandler = new android.os.Handler(android.os.Looper.getMainLooper()); initializeControllers(); } /** * Инициализирует все контроллеры */ private void initializeControllers() { // Инициализация парсера NMEA nmeaParser = new NMEAParser(); nmeaParser.setListener(this); // Инициализация GPS Location Listener (для координат) gpsLocationListener = new GPSLocationListener(context); gpsLocationListener.setCallback(this); // Связываем NMEA парсер с GPS Location Listener для гибридного режима nmeaParser.setGPSLocationListener(gpsLocationListener); nmeaParser.setHybridMode(true); // Инициализация UDP слушателя (порт 10110 - стандартный для AIS) udpPort = 10110; udpListener = new UDPListener(udpPort); udpListener.setCallback(this); // Инициализация Android NMEA слушателя (для курса, скорости, DOP) androidNmeaListener = new AndroidNMEAListener(context); androidNmeaListener.setCallback(this); // Восстанавливаем данные из БД при старте АСИНХРОННО Log.i(TAG, "🔄 Запускаем асинхронное восстановление данных из БД..."); executor.execute(() -> { try { Log.d(TAG, "📊 Загружаем данные судна из БД..."); com.grigowashere.aismap.data.entity.VesselEntity latest = repository.getLatestOwnVesselSync(); if (latest != null) { ownVessel.setLatitude(latest.latitude); ownVessel.setLongitude(latest.longitude); ownVessel.setAccuracy(latest.accuracy); ownVessel.setFixTime(latest.fixTime); Log.d(TAG, "✅ Данные судна восстановлены: " + latest.latitude + "," + latest.longitude); } else { Log.d(TAG, "ℹ️ Нет данных судна в БД"); } Log.d(TAG, "🚢 Загружаем AIS суда из БД..."); java.util.List list = repository.getAllAISSync(); if (list != null && !list.isEmpty()) { synchronized (aisVessels) { aisVessels.clear(); // Очищаем перед восстановлением for (com.grigowashere.aismap.data.entity.AISVesselEntity entity : list) { // Используем маппер для полного восстановления всех полей AISVessel vessel = AISVesselMapper.toModel(entity); aisVessels.add(vessel); Log.d(TAG, "AIS судно восстановлено из БД с полными данными: " + vessel.getMmsi()); } } Log.i(TAG, "✅ Восстановлено " + list.size() + " AIS судов из БД с полными данными"); } else { Log.d(TAG, "ℹ️ Нет AIS судов в БД"); } // Уведомляем UI о восстановлении данных (если mapInterface уже установлен) uiHandler.post(() -> { if (mapInterface != null) { Log.i(TAG, "🔄 Уведомляем UI о восстановленных данных..."); // Восстановление маркеров будет выполнено через setMapInterface() // когда он будет вызван из MainActivity } else { Log.d(TAG, "⏳ mapInterface еще не установлен, восстановление отложено"); } }); } catch (Exception e) { Log.e(TAG, "❌ Ошибка восстановления данных из БД: " + e.getMessage(), e); } }); } /** * Устанавливает интерфейс карты */ public void setMapInterface(MapInterface mapInterface) { Log.i(TAG, "setMapInterface вызван: " + (mapInterface != null ? "mapInterface установлен" : "mapInterface == null")); this.mapInterface = mapInterface; if (mapInterface != null) { Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface"); mapInterface.setMarkerClickListener(this); Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры"); // Уведомляем UI Coordinator о восстановлении данных if (uiDataNotifier != null) { Log.i(TAG, "🔄 Восстановление данных через UI Coordinator"); // Восстанавливаем позицию собственного судна if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) { Log.i(TAG, "📍 Восстанавливаем позицию судна: " + ownVessel.getLatitude() + "," + ownVessel.getLongitude()); uiDataNotifier.onVesselPositionChanged(ownVessel); } else { Log.w(TAG, "⚠️ Судно не имеет валидных координат для восстановления"); } // Восстанавливаем AIS суда if (aisVessels != null && !aisVessels.isEmpty()) { Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов"); for (AISVessel v : aisVessels) { Log.d(TAG, " - AIS судно: " + v.getMmsi() + " на " + v.getLatitude() + "," + v.getLongitude()); uiDataNotifier.onAISVesselChanged(v); } Log.i(TAG, "✅ " + aisVessels.size() + " AIS судов отправлено в UI Coordinator"); } else { Log.i(TAG, "ℹ️ Нет AIS судов для восстановления"); } } else { Log.w(TAG, "❌ uiDataNotifier не установлен при восстановлении данных - маркеры НЕ будут восстановлены!"); } } } /** * Устанавливает индикатор изменений данных для централизованного UI throttling */ public void setUIDataChangeNotifier(UIDataChangeNotifier notifier) { this.uiDataNotifier = notifier; Log.i(TAG, "UIDataChangeNotifier установлен: " + (notifier != null ? "success" : "null")); } /** * Устанавливает callback для обновления UI (legacy для MainActivity) */ public void setUIUpdateCallback(UIUpdateCallback callback) { this.uiUpdateCallback = callback; } /** * Запускает все слушатели */ public void startAllListeners() { // GPS Location Listener запускается в главном потоке if (isGPSLocationEnabled) { gpsLocationListener.startListening(); } // Android NMEA слушатель должен запускаться в главном потоке if (isAndroidNMEAEnabled) { androidNmeaListener.startListening(); } // UDP слушатель запускается в фоновом потоке if (isUDPEnabled) { executor.execute(() -> { udpListener.start(); }); } // Запускаем периодическую очистку БД от устаревших AIS целей startDatabaseCleanup(); } /** * Останавливает все слушатели */ public void stopAllListeners() { // Останавливаем периодическую очистку БД stopDatabaseCleanup(); executor.execute(() -> { udpListener.stop(); androidNmeaListener.stopListening(); gpsLocationListener.stopListening(); }); } /** * Включает/выключает UDP слушатель */ public void setUDPEnabled(boolean enabled) { this.isUDPEnabled = enabled; if (enabled && !udpListener.isRunning()) { udpListener.start(); } else if (!enabled && udpListener.isRunning()) { udpListener.stop(); } } /** * Включает/выключает Android NMEA слушатель */ public void setAndroidNMEAEnabled(boolean enabled) { Log.i(TAG, "🔄 setAndroidNMEAEnabled: " + enabled); this.isAndroidNMEAEnabled = enabled; // Android NMEA слушатель управляется в главном потоке if (enabled && !androidNmeaListener.isListening()) { Log.i(TAG, "🚀 Запускаем Android NMEA слушатель..."); boolean success = androidNmeaListener.startListening(); if (success) { Log.i(TAG, "✅ Android NMEA слушатель успешно запущен"); } else { Log.e(TAG, "❌ Не удалось запустить Android NMEA слушатель"); } } else if (!enabled && androidNmeaListener.isListening()) { Log.i(TAG, "⏹️ Останавливаем Android NMEA слушатель..."); androidNmeaListener.stopListening(); } } /** * Включает/выключает GPS Location слушатель */ public void setGPSLocationEnabled(boolean enabled) { Log.i(TAG, "🔄 setGPSLocationEnabled: " + enabled); this.isGPSLocationEnabled = enabled; if (enabled && !gpsLocationListener.isListening()) { Log.i(TAG, "🚀 Запускаем GPS Location слушатель..."); boolean success = gpsLocationListener.startListening(); if (success) { Log.i(TAG, "✅ GPS Location слушатель успешно запущен"); } else { Log.e(TAG, "❌ Не удалось запустить GPS Location слушатель"); } } else if (!enabled && gpsLocationListener.isListening()) { Log.i(TAG, "⏹️ Останавливаем GPS Location слушатель..."); gpsLocationListener.stopListening(); } } /** * Отправляет данные по UDP */ public void sendUDPData(String data, String address, int port) { udpListener.sendData(data, address, port); } /** * Проверяет, включен ли UDP слушатель */ public boolean isUDPEnabled() { return isUDPEnabled; } /** * Проверяет, включен ли Android NMEA слушатель */ public boolean isAndroidNMEAEnabled() { return isAndroidNMEAEnabled; } /** * Проверяет, включен ли GPS Location слушатель */ public boolean isGPSLocationEnabled() { return isGPSLocationEnabled; } /** * Обновляет данные нашего судна при клике по маркеру */ private void updateOwnVesselData(Vessel vessel) { if (vessel != null) { // Обновляем только те данные, которые могут быть актуальными // Координаты и основная информация уже обновляются через GPS if (vessel.getCourse() > 0) { ownVessel.setCourse(vessel.getCourse()); updateCompass(); // Обновляем компас при изменении курса } if (vessel.getSpeed() > 0) { ownVessel.setSpeed(vessel.getSpeed()); } if (vessel.getSatellites() > 0) { ownVessel.setSatellites(vessel.getSatellites()); } if (vessel.getAltitude() != 0) { ownVessel.setAltitude(vessel.getAltitude()); } if (vessel.getPdop() > 0) { ownVessel.setPdop(vessel.getPdop()); ownVessel.setHdop(vessel.getHdop()); ownVessel.setVdop(vessel.getVdop()); } } } // Реализация LocationCallback (GPS Location Listener) @Override public void onLocationUpdated(Vessel vessel) { Log.i(TAG, "📍 GPS Location обновлен: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", accuracy=" + vessel.getAccuracy() + "м"); // Обновляем координаты нашего судна ownVessel.setLatitude(vessel.getLatitude()); ownVessel.setLongitude(vessel.getLongitude()); ownVessel.setAccuracy(vessel.getAccuracy()); ownVessel.setFixTime(vessel.getFixTime()); ownVessel.setFixQuality(vessel.getFixQuality()); // Добавляем точку в путь судна if (pathController != null) { boolean pointAdded = pathController.addPathPoint( vessel.getLongitude(), vessel.getLatitude(), (float) ownVessel.getSpeed() ); if (pointAdded) { Log.d(TAG, "Точка пути добавлена из GPS: " + pathController.getPathPointsCount() + " точек"); } } // Сохраняем позицию в локальную БД try { com.grigowashere.aismap.data.entity.VesselEntity ve = new com.grigowashere.aismap.data.entity.VesselEntity(); ve.latitude = ownVessel.getLatitude(); ve.longitude = ownVessel.getLongitude(); ve.accuracy = ownVessel.getAccuracy(); ve.fixTime = ownVessel.getFixTime(); repository.upsertOwnVessel(ve); } catch (Exception e) { Log.e(TAG, "Ошибка сохранения позиции в БД: " + e.getMessage(), e); } // Обновляем UI через callback if (uiUpdateCallback != null) { uiUpdateCallback.onVesselPositionUpdated(ownVessel); } // Уведомляем UI Coordinator об изменении позиции судна (централизованный throttling) if (uiDataNotifier != null) { Log.d(TAG, "Уведомляем UI Coordinator об изменении позиции судна"); uiDataNotifier.onVesselPositionChanged(ownVessel); } else { Log.w(TAG, "uiDataNotifier не установлен, пропускаем UI обновление"); } } @Override public void onGPSStatusChanged(int status) { Log.i(TAG, "GPS статус изменился: " + status); } // Реализация NMEAParserListener @Override public void onVesselUpdated(Vessel vessel) { // Сокращаем шум логов: подробности обновления судна убраны // Обновляем координаты, если они есть (для режима "только NMEA") if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { ownVessel.setLatitude(vessel.getLatitude()); ownVessel.setLongitude(vessel.getLongitude()); // Сокращаем шум логов: координаты обновлены (без детализации) // Добавляем точку в путь судна if (pathController != null) { boolean pointAdded = pathController.addPathPoint( vessel.getLongitude(), vessel.getLatitude(), (float) vessel.getSpeed() ); // Убираем лог о добавлении каждой точки пути } } // Обновляем дополнительные данные if (vessel.getCourse() > 0) { ownVessel.setCourse(vessel.getCourse()); updateCompass(); // Обновляем компас при изменении курса } if (vessel.getSpeed() > 0) { ownVessel.setSpeed(vessel.getSpeed()); } if (vessel.getSatellites() > 0) { ownVessel.setSatellites(vessel.getSatellites()); } if (vessel.getAltitude() != 0) { ownVessel.setAltitude(vessel.getAltitude()); } // Сокращаем шум логов: сводка NMEA обновлений убрана // Обновляем карту в главном потоке if (mapInterface != null) { // Сокращаем шум логов: убираем информационные логи карты uiHandler.post(() -> { try { mapInterface.updateOwnVesselPosition(ownVessel); } catch (Exception e) { Log.e(TAG, "Ошибка обновления позиции на карте из NMEA: " + e.getMessage(), e); } }); } // Обновляем UI if (uiUpdateCallback != null) { uiUpdateCallback.onVesselPositionUpdated(ownVessel); } } @Override public void onDOPUpdated(double pdop, double hdop, double vdop) { // Убираем шумный лог DOP обновлений // Обновляем DOP значения ownVessel.setPdop(pdop); ownVessel.setHdop(hdop); ownVessel.setVdop(vdop); // Обновляем UI if (uiUpdateCallback != null) { uiUpdateCallback.onGPSQualityUpdated(ownVessel); } } @Override public void onAISVesselUpdated(AISVessel vessel) { // Проверяем, есть ли уже такое судно AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi()); if (existingVessel != null) { // Если пришло новое safety-сообщение (тип 14), уведомим пользователя if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) { String prev = existingVessel.getLastSafetyMessage(); String curr = vessel.getLastSafetyMessage(); if (prev == null || !prev.equals(curr)) { if (notificationService != null && notificationService.areNotificationsEnabled()) { notificationService.notifySafetyMessage(vessel.getMmsi(), curr); } } existingVessel.setLastSafetyMessage(curr); } // Обновляем существующее судно existingVessel.updatePosition( vessel.getLatitude(), vessel.getLongitude(), vessel.getCourse(), vessel.getSpeed() ); try { // Используем маппер для полной конвертации всех полей com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(existingVessel); repository.upsertAIS(entity); Log.d(TAG, "AIS судно сохранено в БД с полными данными: " + existingVessel.getMmsi()); } catch (Exception e) { Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e); } // Добавляем точку в путь AIS судна addAISVesselPathPoint(existingVessel); // Уведомляем UI Coordinator об обновлении AIS судна if (uiDataNotifier != null) { Log.d(TAG, "Уведомляем UI Coordinator об обновлении AIS судна: " + existingVessel.getMmsi()); uiDataNotifier.onAISVesselChanged(existingVessel); } else { Log.w(TAG, "uiDataNotifier не установлен, пропускаем AIS обновление"); } } else { // Добавляем новое судно aisVessels.add(vessel); // Если это новое судно сразу пришло с safety-сообщением — уведомим if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) { if (notificationService != null && notificationService.areNotificationsEnabled()) { notificationService.notifySafetyMessage(vessel.getMmsi(), vessel.getLastSafetyMessage()); } } // Воспроизводим уведомление о новой цели if (notificationService != null && notificationService.areNotificationsEnabled()) { notificationService.notifyNewAISTarget(); Log.i(TAG, "🔔 Уведомление о новой AIS цели: " + vessel.getMmsi()); } try { // Используем маппер для полной конвертации всех полей com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(vessel); repository.upsertAIS(entity); Log.d(TAG, "Новое AIS судно сохранено в БД с полными данными: " + vessel.getMmsi()); } catch (Exception e) { Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e); } // Добавляем точку в путь нового AIS судна addAISVesselPathPoint(vessel); // Уведомляем UI Coordinator о новом AIS судне if (uiDataNotifier != null) { Log.d(TAG, "Уведомляем UI Coordinator о новом AIS судне: " + vessel.getMmsi()); uiDataNotifier.onAISVesselChanged(vessel); } else { Log.w(TAG, "uiDataNotifier не установлен, пропускаем добавление AIS судна"); } } // Обновляем компас с ближайшими судами updateCompass(); Log.i(TAG, "AIS судно обновлено: " + vessel); } @Override public void onParseError(String error) { Log.e(TAG, "Ошибка парсинга NMEA: " + error); } /** * Обновляет компас с текущим азимутом и ближайшими судами */ private void updateCompass() { if (uiUpdateCallback instanceof ExtendedUIUpdateCallback) { float azimuth = (float) ownVessel.getCourse(); List nearbyVessels = getNearbyVessels(); // Используем существующий uiHandler вместо создания нового uiHandler.post(() -> { ((ExtendedUIUpdateCallback) uiUpdateCallback).onUpdateCompass(azimuth, nearbyVessels); }); } } /** * Получает список ближайших судов (в пределах 10 км) */ private List getNearbyVessels() { List nearby = new ArrayList<>(); double maxDistance = 10000; // 10 км в метрах for (AISVessel vessel : aisVessels) { double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel); if (distance <= maxDistance) { nearby.add(vessel); } } return nearby; } // Реализация UDPListenerCallback @Override public void onDataReceived(String data, String sourceAddress, int sourcePort) { // Диагностика: логируем каждые 10 секунд long now = System.currentTimeMillis(); if (now - lastServiceLogTime > 10000) { Log.d(TAG, "📡 AppController: UDP данные получены от " + sourceAddress + ":" + sourcePort); lastServiceLogTime = now; } // Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ executor.execute(() -> { try { nmeaParser.parseNMEA(data); // Диагностика: логируем каждые 10 секунд long now2 = System.currentTimeMillis(); if (now2 - lastServiceLogTime > 10000) { Log.d(TAG, "✅ AppController: UDP NMEA обработано в фоновом потоке"); lastServiceLogTime = now2; } } catch (Exception e) { Log.e(TAG, "❌ Ошибка парсинга UDP NMEA в фоновом потоке: " + e.getMessage(), e); } }); // Обновляем метки времени по префиксу в UI потоке (быстрая операция) updateLastMessageAgesFromRaw(data); } @Override public void onUDPError(String error) { Log.e(TAG, "UDP ошибка: " + error); } @Override public void onError(String error) { Log.e(TAG, "GPS Location ошибка: " + error); } // Реализация NMEAMessageCallback @Override public void onNMEAMessage(String message, long timestamp) { // Диагностика: логируем каждые 10 секунд long now = System.currentTimeMillis(); if (now - lastServiceLogTime > 10000) { Log.d(TAG, "📱 AppController: Android NMEA сообщение получено"); lastServiceLogTime = now; } // Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ executor.execute(() -> { try { nmeaParser.parseNMEA(message); // Диагностика: логируем каждые 10 секунд long now2 = System.currentTimeMillis(); if (now2 - lastServiceLogTime > 10000) { Log.d(TAG, "✅ AppController: NMEA обработано в фоновом потоке"); lastServiceLogTime = now2; } } catch (Exception e) { Log.e(TAG, "❌ Ошибка парсинга NMEA в фоновом потоке: " + e.getMessage(), e); } }); // Обновляем метки времени в UI потоке (быстрая операция) if (message != null) { String trimmed = message.trim(); if (!trimmed.isEmpty()) { char c = trimmed.charAt(0); long now3 = android.os.SystemClock.elapsedRealtime(); if (c == '$') { lastGPSMessageRealtimeMs = now3; } else if (c == '!') { lastAISMessageRealtimeMs = now3; } } } } // Реализация MarkerClickListener @Override public void onOwnVesselClick(Vessel vessel) { Log.i(TAG, "Клик по нашему судну: " + vessel); // Уведомляем UI о необходимости показать BottomSheet if (uiUpdateCallback != null) { Log.i(TAG, "uiUpdateCallback найден, обновляем данные судна"); // Обновляем данные судна перед показом updateOwnVesselData(vessel); // Вызываем специальный callback для показа BottomSheet if (uiUpdateCallback instanceof ExtendedUIUpdateCallback) { Log.i(TAG, "Вызываем onShowOwnVesselBottomSheet"); ((ExtendedUIUpdateCallback) uiUpdateCallback).onShowOwnVesselBottomSheet(); } else { Log.w(TAG, "uiUpdateCallback не является ExtendedUIUpdateCallback"); } } else { Log.e(TAG, "uiUpdateCallback == null!"); } } @Override public void onAISVesselClick(AISVessel vessel) { Log.i(TAG, "Клик по AIS судну: " + vessel); // Уведомляем UI о необходимости показать информацию об AIS судне if (uiUpdateCallback != null && uiUpdateCallback instanceof ExtendedUIUpdateCallback) { ((ExtendedUIUpdateCallback) uiUpdateCallback).onShowAISVesselInfo(vessel); } } /** * Находит AIS судно по MMSI */ private AISVessel findAISVesselByMMSI(String mmsi) { for (AISVessel vessel : aisVessels) { if (mmsi.equals(vessel.getMmsi())) { return vessel; } } return null; } /** * Получает наше судно */ public Vessel getOwnVessel() { return ownVessel; } /** * Получает список AIS судов */ public List getAISVessels() { return new ArrayList<>(aisVessels); } /** * Очищает все AIS суда */ public void clearAISVessels() { Log.i(TAG, "Очищаем AIS суда из контроллера"); // Очищаем локальные данные aisVessels.clear(); // Уведомляем UI Coordinator о необходимости очистки карты if (uiDataNotifier != null) { Log.d(TAG, "Уведомляем UI Coordinator об очистке AIS судов"); // TODO: Добавить метод очистки всех AIS судов в UIDataChangeNotifier // Пока что очищаем через individual removals Log.i(TAG, "Individual AIS removal через uiDataNotifier еще не реализован"); } else { Log.w(TAG, "uiDataNotifier не установлен, очистка AIS судов пропущена"); } // Очищаем AIS path controllers aisPathControllers.clear(); } /** * Центрирует карту на позиции нашего судна */ public void centerOnOwnVessel() { if (ownVessel != null) { Log.d(TAG, "Запрос центрирования карты на судне: " + ownVessel.getLatitude() + "," + ownVessel.getLongitude()); // Уведомляем UI Coordinator о необходимости центрирования карты if (uiDataNotifier != null) { uiDataNotifier.onRequestCenterMap(ownVessel.getLatitude(), ownVessel.getLongitude()); } else { Log.w(TAG, "uiDataNotifier не установлен, центрирование карты пропущено"); } } } /** * Запускает периодическую очистку БД от устаревших AIS целей */ public void startDatabaseCleanup() { if (dbCleanupHandler != null && dbCleanupRunnable != null) { dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL); Log.i(TAG, "Запущена периодическая очистка БД от устаревших AIS целей"); } } /** * Останавливает периодическую очистку БД */ public void stopDatabaseCleanup() { if (dbCleanupHandler != null && dbCleanupRunnable != null) { dbCleanupHandler.removeCallbacks(dbCleanupRunnable); Log.i(TAG, "Остановлена периодическая очистка БД от устаревших AIS целей"); } } /** * Выполняет очистку БД от устаревших AIS целей */ private void performDatabaseCleanup() { try { com.grigowashere.aismap.utils.SettingsManager settingsManager = new com.grigowashere.aismap.utils.SettingsManager(context); int staleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes(); long thresholdEpochMs = System.currentTimeMillis() - (staleRemoveMinutes * 60 * 1000L); repository.deleteStaleAIS(thresholdEpochMs); Log.i(TAG, "Выполнена очистка БД от AIS целей старше " + staleRemoveMinutes + " минут"); // Планируем следующую очистку if (dbCleanupHandler != null && dbCleanupRunnable != null) { dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL); } } catch (Exception e) { Log.e(TAG, "Ошибка при очистке БД от устаревших AIS целей: " + e.getMessage(), e); } } /** * Освобождает ресурсы */ public void cleanup() { stopAllListeners(); stopDatabaseCleanup(); // Очищаем Handler'ы для предотвращения утечек памяти if (uiHandler != null) { uiHandler.removeCallbacksAndMessages(null); } if (udpListener != null) { udpListener.cleanup(); } if (androidNmeaListener != null) { androidNmeaListener.cleanup(); } if (gpsLocationListener != null) { gpsLocationListener.cleanup(); } if (notificationService != null) { notificationService.cleanup(); } if (executor != null && !executor.isShutdown()) { executor.shutdown(); } } // ===== Метки времени последних сообщений ($ и !) ===== private void updateLastMessageAgesFromRaw(String raw) { if (raw == null) return; long now = android.os.SystemClock.elapsedRealtime(); String[] lines = raw.split("\r?\n"); for (String line : lines) { if (line == null) continue; String t = line.trim(); if (t.isEmpty()) continue; char c = t.charAt(0); if (c == '$') { lastGPSMessageRealtimeMs = now; break; } else if (c == '!') { lastAISMessageRealtimeMs = now; break; } } } /** Возвращает секунды с последнего GPS ($) сообщения; -1 если не было */ public int getSecondsSinceLastGPSMessage() { if (lastGPSMessageRealtimeMs <= 0) return -1; long diff = android.os.SystemClock.elapsedRealtime() - lastGPSMessageRealtimeMs; if (diff < 0) return 0; return (int)(diff / 1000L); } /** Возвращает секунды с последнего AIS (!) сообщения; -1 если не было */ public int getSecondsSinceLastAISMessage() { if (lastAISMessageRealtimeMs <= 0) return -1; long diff = android.os.SystemClock.elapsedRealtime() - lastAISMessageRealtimeMs; if (diff < 0) return 0; return (int)(diff / 1000L); } // Методы для управления настройками /** * Устанавливает UDP порт */ public void setUDPPort(int port) { if (port < 1 || port > 65535) { Log.w(TAG, "Некорректный UDP порт: " + port); return; } this.udpPort = port; Log.i(TAG, "UDP порт установлен: " + port); // Если UDP слушатель уже создан, нужно будет его пересоздать if (udpListener != null && udpListener.getPort() != port) { Log.i(TAG, "UDP порт изменен, потребуется перезапуск UDP слушателя"); } } /** * Получает текущий UDP порт */ public int getUDPPort() { return udpPort; } /** * Включает/выключает UDP NMEA */ public void setUDPNMEAEnabled(boolean enabled) { this.isUDPNMEAEnabled = enabled; Log.i(TAG, "UDP NMEA: " + (enabled ? "включен" : "выключен")); } /** * Проверяет, включен ли UDP NMEA */ public boolean isUDPNMEAEnabled() { return isUDPNMEAEnabled; } /** * Устанавливает режим работы с данными */ public void setDataMode(String mode) { this.dataMode = mode; Log.i(TAG, "🔄 Режим данных установлен: " + mode); // Применяем режим к NMEA парсеру if (nmeaParser != null) { boolean hybridMode = "hybrid".equals(mode); nmeaParser.setHybridMode(hybridMode); Log.i(TAG, "📍 Гибридный режим NMEA парсера: " + hybridMode); Log.i(TAG, "📍 В режиме '" + mode + "' координаты будут " + (hybridMode ? "браться из Android GPS API" : "браться из NMEA сообщений")); } } /** * Получает текущий режим работы с данными */ public String getDataMode() { return dataMode; } /** * Перезапускает UDP слушатель с новым портом */ public void restartUDPListener() { if (udpListener != null) { Log.i(TAG, "Перезапускаем UDP слушатель с портом: " + udpPort); // Останавливаем текущий слушатель udpListener.stop(); udpListener.cleanup(); // Создаем новый слушатель с новым портом udpListener = new UDPListener(udpPort); udpListener.setCallback(this); // Запускаем, если UDP включен if (isUDPEnabled) { udpListener.start(); Log.i(TAG, "UDP слушатель перезапущен на порту: " + udpPort); } } } /** * Получает статус всех настроек */ public String getSettingsStatus() { return String.format( "UDP: порт=%d, включен=%s, NMEA=%s\n" + "Android NMEA: %s\n" + "GPS Location: %s\n" + "Режим данных: %s", udpPort, isUDPEnabled ? "да" : "нет", isUDPNMEAEnabled ? "включен" : "выключен", isAndroidNMEAEnabled ? "включен" : "выключен", isGPSLocationEnabled ? "включен" : "выключен", dataMode != null ? dataMode : "не установлен" ); } // ===== Методы для работы с путем судна ===== /** * Получает контроллер пути судна */ public VesselPathController getPathController() { return pathController; } /** * Получает информацию о пути судна */ public String getVesselPathInfo() { if (pathController != null) { return pathController.getPathInfo(); } return "Контроллер пути не инициализирован"; } /** * Очищает путь судна */ public void clearVesselPath() { if (pathController != null) { pathController.clearPath(); Log.i(TAG, "Путь судна очищен"); } } /** * Сохраняет путь судна */ public void saveVesselPath() { if (pathController != null) { Log.d(TAG, "Сохранение пути судна: " + pathController.getPathInfo()); } } /** * Добавляет точку в путь AIS судна */ private void addAISVesselPathPoint(AISVessel vessel) { if (vessel == null || vessel.getMmsi() == null) { return; } // Проверяем валидность координат if (!isValidCoordinates(vessel.getLatitude(), vessel.getLongitude())) { Log.d(TAG, "addAISVesselPathPoint: AIS vessel " + vessel.getMmsi() + " has invalid coordinates " + vessel.getLatitude() + "," + vessel.getLongitude() + " - skipping path point"); return; } String mmsi = vessel.getMmsi(); // Получаем или создаем VesselPathController для этого AIS судна VesselPathController aisPathController = aisPathControllers.get(mmsi); if (aisPathController == null) { // Ограничиваем количество трекеров для производительности if (aisPathControllers.size() >= 20) { Log.w(TAG, "Достигнуто максимальное количество AIS трекеров (20), пропускаем создание для " + mmsi); return; } aisPathController = new VesselPathController(context, settingsManager, mmsi); aisPathControllers.put(mmsi, aisPathController); Log.d(TAG, "Создан VesselPathController для AIS судна " + mmsi + " (всего трекеров: " + aisPathControllers.size() + ")"); } // Добавляем точку в путь boolean pointAdded = aisPathController.addPathPoint( vessel.getLongitude(), vessel.getLatitude(), (float) vessel.getSpeed() ); if (pointAdded) { Log.d(TAG, "Точка пути добавлена для AIS " + mmsi + ": " + aisPathController.getPathPointsCount() + " точек"); } } /** * Проверяет валидность координат * Игнорирует координаты 0,0 и 181,91 (невалидные значения AIS) */ private boolean isValidCoordinates(double latitude, double longitude) { // Проверяем на нулевые координаты if (latitude == 0.0 && longitude == 0.0) { return false; } // Проверяем на невалидные координаты AIS (181, 91) if (latitude == 91.0 && longitude == 181.0) { return false; } // Проверяем на стандартные границы координат if (latitude < -90.0 || latitude > 90.0) { return false; } if (longitude < -180.0 || longitude > 180.0) { return false; } return true; } /** * Получает VesselPathController для AIS судна */ public VesselPathController getAISVesselPathController(String mmsi) { return aisPathControllers.get(mmsi); } /** * Очищает путь AIS судна */ public void clearAISVesselPath(String mmsi) { VesselPathController aisPathController = aisPathControllers.get(mmsi); if (aisPathController != null) { aisPathController.clearPath(); Log.d(TAG, "Путь AIS судна " + mmsi + " очищен"); } } /** * Очищает все пути AIS судов */ public void clearAllAISVesselPaths() { for (VesselPathController controller : aisPathControllers.values()) { controller.clearPath(); } aisPathControllers.clear(); Log.d(TAG, "Все пути AIS судов очищены"); } }