Files
AndroidAisMap/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java
T
Grigo b5aee265bc feat: новая архитектура UI и расширенная визуализация AIS
Архитектурные улучшения:
- Внедрен UIRenderingCoordinator с централизованным throttling
- Решены проблемы зависания UI через батчинг операций карты
- Добавлен VesselPathController для отслеживания маршрутов
- Реализован MapLibreMapImpl как альтернатива Яндекс.Картам

Визуализация AIS:
- Добавлены векторные иконки для всех типов судов
- Разделение Class A/B судов с соответствующими иконками
- Иконки навигационных статусов (anchor, moored, engine, sail)
- Улучшенный CursorOverlay с информацией о судах

Производительность:
- Throttling UI обновлений (vessel: 500ms, AIS: 1s, paths: 2s)
- Устранение утечек Handler объектов
- Оптимизация GeoJSON операций в MapLibre
2025-10-02 09:15:33 +03:00

1187 lines
50 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AISVessel> 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<String, VesselPathController> 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<AISVessel> 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<com.grigowashere.aismap.data.entity.AISVesselEntity> 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<AISVessel> nearbyVessels = getNearbyVessels();
// Используем существующий uiHandler вместо создания нового
uiHandler.post(() -> {
((ExtendedUIUpdateCallback) uiUpdateCallback).onUpdateCompass(azimuth, nearbyVessels);
});
}
}
/**
* Получает список ближайших судов (в пределах 10 км)
*/
private List<AISVessel> getNearbyVessels() {
List<AISVessel> 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<AISVessel> 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 судов очищены");
}
}