Major architecture update

This commit is contained in:
2025-10-07 09:34:26 +03:00
parent 982e940b8d
commit a607133032
32 changed files with 6439 additions and 2061 deletions
File diff suppressed because it is too large Load Diff
@@ -229,6 +229,16 @@ public class AndroidNMEAListener implements OnNmeaMessageListener {
try {
//Log.i(TAG, "🔧 Запрашиваем обновления локации для активации NMEA...");
// Проверка разрешений на локацию перед запросом обновлений
int fine = context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION);
int coarse = context.checkSelfPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION);
if (fine != android.content.pm.PackageManager.PERMISSION_GRANTED &&
coarse != android.content.pm.PackageManager.PERMISSION_GRANTED) {
// Нет нужных разрешений — не запрашиваем обновления, избежим SecurityException и варнингов IDE
Log.w(TAG, "Нет разрешений ACCESS_FINE/COARSE_LOCATION — пропускаем requestLocationUpdates()");
return;
}
// Создаем слушатель локации с минимальными интервалами
locationListener = new LocationListener() {
@Override
@@ -267,6 +277,7 @@ public class AndroidNMEAListener implements OnNmeaMessageListener {
// Дополнительно запрашиваем одиночное обновление для принудительной активации
try {
if (fine == android.content.pm.PackageManager.PERMISSION_GRANTED) {
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER,
new LocationListener() {
@Override public void onLocationChanged(android.location.Location location) {
@@ -276,6 +287,7 @@ public class AndroidNMEAListener implements OnNmeaMessageListener {
@Override public void onProviderEnabled(String provider) {}
@Override public void onProviderDisabled(String provider) {}
}, Looper.getMainLooper()); // Looper вместо Handler
}
//Log.i(TAG, "✅ Одиночное обновление запрошено");
} catch (Exception e) {
//Log.w(TAG, "⚠️ Не удалось запросить одиночное обновление: " + e.getMessage());
@@ -284,6 +296,8 @@ public class AndroidNMEAListener implements OnNmeaMessageListener {
// Дополнительно запрашиваем обновления от всех доступных провайдеров
try {
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
if (fine == android.content.pm.PackageManager.PERMISSION_GRANTED ||
coarse == android.content.pm.PackageManager.PERMISSION_GRANTED) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
1000L, // 1 секунда
@@ -291,6 +305,7 @@ public class AndroidNMEAListener implements OnNmeaMessageListener {
locationListener,
Looper.getMainLooper()
);
}
//Log.i(TAG, "✅ Network провайдер также активирован");
}
} catch (Exception e) {
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,808 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
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.maps.MapInterfaceChangeListener;
import com.grigowashere.aismap.ui.UIDataChangeNotifier;
import com.grigowashere.aismap.utils.SettingsManager;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
/**
* Главный координатор приложения
* Координирует работу всех контроллеров и управляет общим состоянием
*/
public class AppCoordinator implements
NMEAController.NMEAControllerListener,
NetworkController.NetworkControllerListener,
DataController.DataControllerListener,
NotificationController.NotificationControllerListener,
CompassController.CompassControllerListener,
MapInterface.MarkerClickListener,
MapInterfaceChangeListener {
private static final String TAG = "AppCoordinator";
private Context context;
// Контроллеры
private NMEAController nmeaController;
private NetworkController networkController;
private DataController dataController;
private NotificationController notificationController;
private CompassController compassController;
private MapController mapController;
// Состояние приложения
private Vessel ownVessel;
private List<AISVessel> aisVessels;
private Map<String, VesselPathController> aisPathControllers;
private SettingsManager settingsManager;
private VesselPathController pathController;
// UI координация
private UIDataChangeNotifier uiDataNotifier;
private Handler uiHandler;
// Callbacks для уведомления UI
private AppCoordinatorListener listener;
// Диагностика
private long lastServiceLogTime = 0;
// Время последнего получения сообщений ($ GPS) и (! AIS) в elapsedRealtime
private long lastGPSMessageRealtimeMs;
private long lastAISMessageRealtimeMs;
public interface AppCoordinatorListener {
void onVesselPositionUpdated(Vessel vessel);
void onGPSQualityUpdated(Vessel vessel);
void onShowOwnVesselBottomSheet();
void onShowAISVesselInfo(AISVessel vessel);
void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels);
}
public AppCoordinator(Context context) {
this.context = context;
this.ownVessel = new Vessel();
this.aisVessels = new ArrayList<>();
this.aisPathControllers = new HashMap<>();
this.settingsManager = new SettingsManager(context);
this.pathController = new VesselPathController(context, settingsManager);
this.uiHandler = new Handler(Looper.getMainLooper());
initializeControllers();
Log.i(TAG, "App Coordinator инициализирован");
}
/**
* Инициализирует все контроллеры
*/
private void initializeControllers() {
// Создаем контроллеры
nmeaController = new NMEAController(context);
networkController = new NetworkController(context);
dataController = new DataController(context);
notificationController = new NotificationController(context);
compassController = new CompassController(context);
// Подписываемся на события контроллеров
nmeaController.setListener(this);
networkController.setListener(this);
dataController.setListener(this);
notificationController.setListener(this);
compassController.setListener(this);
Log.i(TAG, "Все контроллеры инициализированы и подключены");
}
/**
* Устанавливает слушателя для уведомлений
*/
public void setListener(AppCoordinatorListener listener) {
this.listener = listener;
}
/**
* Устанавливает MapController
*/
public void setMapController(MapController mapController) {
// Отписываемся от старого MapController
if (this.mapController != null) {
this.mapController.removeMapInterfaceChangeListener(this);
}
this.mapController = mapController;
// Подписываемся на новый MapController
if (mapController != null) {
mapController.addMapInterfaceChangeListener(this);
Log.i(TAG, "AppCoordinator подключен к MapController");
}
}
/**
* Устанавливает UI Data Change Notifier
*/
public void setUIDataChangeNotifier(UIDataChangeNotifier notifier) {
this.uiDataNotifier = notifier;
Log.i(TAG, "UIDataChangeNotifier установлен: " + (notifier != null ? "success" : "null"));
}
/**
* Запускает все контроллеры
*/
public void startAllControllers() {
Log.i(TAG, "🚀 Запускаем все контроллеры...");
// Запускаем контроллеры на основе настроек
startControllersBasedOnSettings();
networkController.startUDPListener();
compassController.startCompass();
dataController.startDatabaseCleanup();
// Восстанавливаем данные из БД
dataController.restoreDataAsync();
Log.i(TAG, "✅ Все контроллеры запущены");
}
/**
* Запускает контроллеры на основе настроек данных
*/
private void startControllersBasedOnSettings() {
String dataMode = settingsManager.getDataMode();
Log.i(TAG, "📍 Запускаем контроллеры в режиме: " + dataMode);
boolean androidNMEA = settingsManager.isAndroidNMEAEnabled();
boolean androidGPS = settingsManager.isAndroidGPSEnabled();
if ("hybrid".equals(dataMode)) {
Log.i(TAG, "📍 Гибридный режим: Android GPS + NMEA");
if (androidNMEA) nmeaController.startAndroidNMEAListener();
if (androidGPS) nmeaController.startGPSLocationListener();
} else if ("android_only".equals(dataMode)) {
Log.i(TAG, "📍 Режим Android GPS: только встроенный GPS");
if (androidGPS) nmeaController.startGPSLocationListener();
} else if ("nmea_only".equals(dataMode)) {
Log.i(TAG, "📍 Режим NMEA: только внешний NMEA");
if (androidNMEA) nmeaController.startAndroidNMEAListener();
} else {
Log.i(TAG, "📍 Режим по умолчанию: гибридный");
if (androidNMEA) nmeaController.startAndroidNMEAListener();
if (androidGPS) nmeaController.startGPSLocationListener();
}
}
/**
* Останавливает все контроллеры
*/
public void stopAllControllers() {
Log.i(TAG, "⏹️ Останавливаем все контроллеры...");
nmeaController.stopAndroidNMEAListener();
nmeaController.stopGPSLocationListener();
networkController.stopUDPListener();
compassController.stopCompass();
dataController.stopDatabaseCleanup();
Log.i(TAG, "✅ Все контроллеры остановлены");
}
/**
* Применяет настройки ко всем контроллерам
*/
public void applySettings() {
// Применяем настройки к контроллерам
String dataMode = settingsManager.getDataMode();
nmeaController.setDataMode(dataMode);
boolean udpEnabled = settingsManager.isUDPEnabled();
networkController.setUDPEnabled(udpEnabled);
int udpPort = settingsManager.getUDPPort();
networkController.setUDPPort(udpPort);
boolean notificationsEnabled = settingsManager.areNotificationsEnabled();
notificationController.setNotificationsEnabled(notificationsEnabled);
// Перезапускаем контроллеры данных на основе новых настроек
restartDataControllers();
Log.i(TAG, "Настройки применены ко всем контроллерам");
}
/**
* Перезапускает контроллеры данных на основе текущих настроек
*/
private void restartDataControllers() {
Log.i(TAG, "🔄 Перезапускаем контроллеры данных...");
// Останавливаем текущие контроллеры данных
nmeaController.stopAndroidNMEAListener();
nmeaController.stopGPSLocationListener();
// Запускаем с новыми настройками
startControllersBasedOnSettings();
Log.i(TAG, "✅ Контроллеры данных перезапущены");
}
// Реализация NMEAControllerListener
@Override
public void onGPSLocationUpdated(Vessel vessel) {
// Обновляем ownVessel координатами от Android GPS (в hybrid/android_only)
if (vessel != null) {
this.ownVessel = vessel;
if (listener != null) {
listener.onVesselPositionUpdated(vessel);
}
if (uiDataNotifier != null) {
uiDataNotifier.onVesselPositionChanged(vessel);
}
}
}
@Override
public void onVesselUpdated(Vessel vessel) {
// Обновляем координаты, если они есть
if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) {
ownVessel.setLatitude(vessel.getLatitude());
ownVessel.setLongitude(vessel.getLongitude());
// Добавляем точку в путь судна
if (pathController != null) {
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());
}
// Сохраняем в БД
dataController.saveVesselPosition(ownVessel);
// Уведомляем UI
if (listener != null) {
listener.onVesselPositionUpdated(ownVessel);
}
if (uiDataNotifier != null) {
uiDataNotifier.onVesselPositionChanged(ownVessel);
}
}
@Override
public void onDOPUpdated(double pdop, double hdop, double vdop) {
ownVessel.setPdop(pdop);
ownVessel.setHdop(hdop);
ownVessel.setVdop(vdop);
if (listener != null) {
listener.onGPSQualityUpdated(ownVessel);
}
}
@Override
public void onAISVesselUpdated(AISVessel vessel) {
// Проверяем, есть ли уже такое судно
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
if (existingVessel != null) {
// Если пришло новое safety-сообщение, уведомим пользователя
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
String prev = existingVessel.getLastSafetyMessage();
String curr = vessel.getLastSafetyMessage();
if (prev == null || !prev.equals(curr)) {
notificationController.notifySafetyMessage(vessel.getMmsi(), curr);
}
existingVessel.setLastSafetyMessage(curr);
}
// Обновляем существующее судно
existingVessel.updatePosition(
vessel.getLatitude(),
vessel.getLongitude(),
vessel.getCourse(),
vessel.getSpeed()
);
// Сохраняем в БД
dataController.saveAISVessel(existingVessel);
// Добавляем точку в путь AIS судна
addAISVesselPathPoint(existingVessel);
// Уведомляем UI Coordinator об обновлении AIS судна
if (uiDataNotifier != null) {
uiDataNotifier.onAISVesselChanged(existingVessel);
}
} else {
// Добавляем новое судно
synchronized (aisVessels) {
aisVessels.add(vessel);
}
// Если это новое судно сразу пришло с safety-сообщением — уведомим
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
notificationController.notifySafetyMessage(vessel.getMmsi(), vessel.getLastSafetyMessage());
}
// Воспроизводим уведомление о новой цели
notificationController.notifyNewAISTarget();
// Сохраняем в БД
dataController.saveAISVessel(vessel);
// Добавляем точку в путь нового AIS судна
addAISVesselPathPoint(vessel);
// Уведомляем UI Coordinator о новом AIS судне
if (uiDataNotifier != null) {
uiDataNotifier.onAISVesselChanged(vessel);
}
}
// Обновляем компас с ближайшими судами
updateCompass();
}
@Override
public void onParseError(String error) {
Log.e(TAG, "Ошибка парсинга NMEA: " + error);
}
// Реализация NetworkControllerListener
@Override
public void onDataReceived(String data, String sourceAddress, int sourcePort) {
// Обновляем метки времени по префиксу в UI потоке (быстрая операция)
updateLastMessageAgesFromRaw(data);
// Передаем данные в NMEA контроллер для парсинга
nmeaController.parseNMEAMessage(data);
}
@Override
public void onUDPError(String error) {
Log.e(TAG, "UDP ошибка: " + error);
notificationController.notifyConnectionError(error);
}
// Реализация DataControllerListener
@Override
public void onDataRestored(Vessel vessel, List<AISVessel> aisVessels) {
if (vessel != null) {
this.ownVessel = vessel;
}
if (aisVessels != null) {
synchronized (this.aisVessels) {
this.aisVessels.clear();
this.aisVessels.addAll(aisVessels);
}
}
Log.i(TAG, "Данные восстановлены из БД");
}
@Override
public void onDataSaved(String dataType, boolean success) {
Log.d(TAG, "Данные сохранены: " + dataType + " = " + success);
}
@Override
public void onDataCleaned(int removedCount) {
Log.i(TAG, "Очищено " + removedCount + " устаревших записей из БД");
}
// Реализация NotificationControllerListener
@Override
public void onNotificationShown(String type, String message) {
Log.d(TAG, "Уведомление показано: " + type + " - " + message);
}
@Override
public void onNotificationError(String error) {
Log.e(TAG, "Ошибка уведомления: " + error);
}
// Реализация CompassControllerListener
@Override
public void onCompassChanged(float azimuth) {
// Обновляем магнитный компас в модели нашего судна
if (ownVessel != null) {
ownVessel.setMagneticCompass(azimuth);
}
// Уведомляем UI
if (listener != null) {
List<AISVessel> nearbyVessels = getNearbyVessels();
listener.onUpdateCompass(azimuth, nearbyVessels);
}
}
@Override
public void onCompassError(String error) {
Log.e(TAG, "Ошибка компаса: " + error);
}
// Реализация MarkerClickListener
@Override
public void onOwnVesselClick(Vessel vessel) {
Log.i(TAG, "Клик по нашему судну: " + vessel);
if (listener != null) {
listener.onShowOwnVesselBottomSheet();
}
}
@Override
public void onAISVesselClick(AISVessel vessel) {
Log.i(TAG, "Клик по AIS судну: " + vessel);
if (listener != null) {
listener.onShowAISVesselInfo(vessel);
}
}
// Реализация MapInterfaceChangeListener
@Override
public void onMapInterfaceChanged(MapInterface oldMapInterface, MapInterface newMapInterface) {
Log.i(TAG, "🔄 MapInterface изменен в MapController");
if (newMapInterface != null) {
// Устанавливаем MarkerClickListener на новую карту
newMapInterface.setMarkerClickListener(this);
Log.i(TAG, "MarkerClickListener установлен на новую карту");
// Восстанавливаем состояние на новой карте
restoreMapStateOnNewInterface();
}
}
/**
* Восстанавливает состояние карты на новом MapInterface
*/
private void restoreMapStateOnNewInterface() {
if (mapController == null || uiDataNotifier == null) {
Log.w(TAG, "⚠️ Нельзя восстановить состояние: mapController=" + (mapController != null) +
", uiDataNotifier=" + (uiDataNotifier != null));
return;
}
Log.i(TAG, "🔄 Восстановление состояния карты на новом MapInterface");
// Восстанавливаем позицию собственного судна
if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) {
Log.i(TAG, "📍 Восстанавливаем позицию судна: " + ownVessel.getLatitude() + "," + ownVessel.getLongitude());
uiDataNotifier.onVesselPositionChanged(ownVessel);
}
// Восстанавливаем AIS суда
if (aisVessels != null && !aisVessels.isEmpty()) {
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
synchronized (aisVessels) {
for (AISVessel vessel : aisVessels) {
uiDataNotifier.onAISVesselChanged(vessel);
}
}
}
Log.i(TAG, "✅ Состояние карты восстановлено");
}
// Вспомогательные методы
private AISVessel findAISVesselByMMSI(String mmsi) {
synchronized (aisVessels) {
for (AISVessel vessel : aisVessels) {
if (mmsi.equals(vessel.getMmsi())) {
return vessel;
}
}
}
return null;
}
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;
}
private void updateCompass() {
if (listener != null) {
float azimuth = (float) ownVessel.getCourse();
List<AISVessel> nearbyVessels = getNearbyVessels();
uiHandler.post(() -> {
listener.onUpdateCompass(azimuth, nearbyVessels);
});
}
}
private void addAISVesselPathPoint(AISVessel vessel) {
if (vessel == null || vessel.getMmsi() == null) {
return;
}
// Проверяем валидность координат
if (!isValidCoordinates(vessel.getLatitude(), vessel.getLongitude())) {
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() + " точек");
// Уведомляем UI о изменении пути AIS судна
if (uiDataNotifier != null) {
uiDataNotifier.onVesselPathChanged(mmsi);
}
}
}
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;
}
// Геттеры для доступа к контроллерам и данным
public Vessel getOwnVessel() {
return ownVessel;
}
public List<AISVessel> getAISVessels() {
synchronized (aisVessels) {
return new ArrayList<>(aisVessels);
}
}
public VesselPathController getPathController() {
return pathController;
}
public VesselPathController getAISVesselPathController(String mmsi) {
return aisPathControllers.get(mmsi);
}
public String getSettingsStatus() {
return String.format(
"NMEA: Android=%s, GPS=%s\n" +
"Network: %s\n" +
"Compass: %s\n" +
"Notifications: %s",
nmeaController.isAndroidNMEAListenerActive() ? "включен" : "выключен",
nmeaController.isGPSLocationListenerActive() ? "включен" : "выключен",
networkController.getNetworkStatus(),
compassController.getCompassStatus(),
notificationController.getNotificationStatus()
);
}
/**
* Возвращает секунды с последнего 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);
}
/**
* Центрирует карту на позиции нашего судна
*/
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 не установлен, центрирование карты пропущено");
}
}
}
/**
* Очищает путь судна
*/
public void clearVesselPath() {
if (pathController != null) {
pathController.clearPath();
Log.i(TAG, "Путь судна очищен");
}
}
/**
* Очищает все AIS суда
*/
public void clearAISVessels() {
Log.i(TAG, "Очищаем AIS суда из координатора");
// Очищаем локальные данные
synchronized (aisVessels) {
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();
}
/**
* Проверяет, включен ли Android NMEA слушатель
*/
public boolean isAndroidNMEAEnabled() {
return nmeaController != null && nmeaController.isAndroidNMEAListenerActive();
}
/**
* Проверяет, включен ли UDP слушатель
*/
public boolean isUDPEnabled() {
return networkController != null && networkController.isUDPEnabled();
}
/**
* Получает текущий UDP порт
*/
public int getUDPPort() {
return networkController != null ? networkController.getUDPPort() : 10110;
}
/**
* Перезапускает UDP слушатель с новым портом
*/
public void restartUDPListener() {
if (networkController != null) {
networkController.restartUDPListener();
}
}
/**
* Обновляет метки времени последних сообщений
*/
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;
}
}
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopAllControllers();
// Очищаем Handler для предотвращения утечек памяти
if (uiHandler != null) {
uiHandler.removeCallbacksAndMessages(null);
}
// Очищаем контроллеры
nmeaController.cleanup();
networkController.cleanup();
dataController.cleanup();
notificationController.cleanup();
compassController.cleanup();
// Отписываемся от MapController
if (mapController != null) {
mapController.removeMapInterfaceChangeListener(this);
}
Log.i(TAG, "App Coordinator очищен");
}
}
@@ -0,0 +1,152 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.grigowashere.aismap.sensors.CompassSensor;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import java.util.List;
/**
* Контроллер для управления магнитным компасом
* Отвечает только за работу с компасом и обновление UI
*/
public class CompassController implements CompassSensor.CompassListener {
private static final String TAG = "CompassController";
private Context context;
private CompassSensor compassSensor;
private Handler uiHandler;
// Диагностика
private long lastCompassLogTime = 0;
// Callbacks для уведомления других компонентов
private CompassControllerListener listener;
public interface CompassControllerListener {
void onCompassChanged(float azimuth);
void onCompassError(String error);
}
public CompassController(Context context) {
this.context = context;
this.compassSensor = new CompassSensor(context);
this.uiHandler = new Handler(Looper.getMainLooper());
Log.i(TAG, "Compass Controller инициализирован");
}
/**
* Устанавливает слушателя для уведомлений
*/
public void setListener(CompassControllerListener listener) {
this.listener = listener;
}
/**
* Запускает магнитный компас
*/
public void startCompass() {
if (compassSensor.isAvailable()) {
compassSensor.startListening(this);
Log.d(TAG, "Magnetic compass started");
} else {
Log.w(TAG, "Magnetic compass not available");
if (listener != null) {
listener.onCompassError("Магнитный компас недоступен на этом устройстве");
}
}
}
/**
* Останавливает магнитный компас
*/
public void stopCompass() {
if (compassSensor.isListening()) {
compassSensor.stopListening();
Log.d(TAG, "Magnetic compass stopped");
}
}
/**
* Проверяет, доступен ли магнитный компас
*/
public boolean isCompassAvailable() {
return compassSensor.isAvailable();
}
/**
* Проверяет, работает ли магнитный компас
*/
public boolean isCompassActive() {
return compassSensor.isListening();
}
/**
* Получает статус компаса
*/
public String getCompassStatus() {
if (!isCompassAvailable()) {
return "Магнитный компас недоступен";
} else if (isCompassActive()) {
return "Магнитный компас активен";
} else {
return "Магнитный компас остановлен";
}
}
/**
* Обновляет компас с данными судна и ближайшими судами
*/
public void updateCompassWithVesselData(Vessel vessel, List<AISVessel> nearbyVessels) {
if (vessel != null && listener != null) {
float azimuth = (float) vessel.getCourse();
// Используем UI Handler для обновления в главном потоке
uiHandler.post(() -> {
listener.onCompassChanged(azimuth);
});
}
}
// Реализация CompassSensor.CompassListener
@Override
public void onCompassChanged(float azimuth) {
// Диагностика: логируем каждые 10 секунд
long now = System.currentTimeMillis();
if (now - lastCompassLogTime > 10000) {
Log.d(TAG, "🧭 CompassController: onCompassChanged получен, azimuth=" + azimuth);
lastCompassLogTime = now;
}
// Уведомляем слушателя в UI потоке
uiHandler.post(() -> {
// Диагностика: проверяем выполнение в UI потоке
if (now - lastCompassLogTime > 10000) {
Log.d(TAG, "🧭 CompassController: runOnUiThread выполняется для компаса");
}
if (listener != null) {
listener.onCompassChanged(azimuth);
}
});
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopCompass();
// Очищаем Handler для предотвращения утечек памяти
if (uiHandler != null) {
uiHandler.removeCallbacksAndMessages(null);
}
Log.i(TAG, "Compass Controller очищен");
}
}
@@ -0,0 +1,16 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
/**
* Фабрика для сборки контроллеров приложения и возврата готового AppCoordinator
*/
public interface ControllersFactory {
/**
* Создает и настраивает все контроллеры, возвращая готовый {@link AppCoordinator}
*/
AppCoordinator createAppCoordinator(Context context);
}
@@ -0,0 +1,252 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.grigowashere.aismap.data.Repository;
import com.grigowashere.aismap.data.entity.VesselEntity;
import com.grigowashere.aismap.data.entity.AISVesselEntity;
import com.grigowashere.aismap.data.mapper.AISVesselMapper;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.utils.SettingsManager;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Контроллер для операций с базой данных
* Отвечает только за сохранение, загрузку и очистку данных
*/
public class DataController {
private static final String TAG = "DataController";
private Context context;
private Repository repository;
private SettingsManager settingsManager;
private ExecutorService executor;
// Периодическая очистка БД от устаревших AIS целей
private Handler dbCleanupHandler;
private Runnable dbCleanupRunnable;
private static final long DB_CLEANUP_INTERVAL = 60000; // 1 минута
// Callbacks для уведомления других компонентов
private DataControllerListener listener;
public interface DataControllerListener {
void onDataRestored(Vessel vessel, List<AISVessel> aisVessels);
void onDataSaved(String dataType, boolean success);
void onDataCleaned(int removedCount);
}
public DataController(Context context) {
this.context = context;
this.repository = new Repository(context);
this.settingsManager = new SettingsManager(context);
this.executor = Executors.newCachedThreadPool();
// Инициализируем Handler для периодической очистки БД
this.dbCleanupHandler = new Handler(Looper.getMainLooper());
this.dbCleanupRunnable = this::performDatabaseCleanup;
Log.i(TAG, "Data Controller инициализирован");
}
/**
* Устанавливает слушателя для уведомлений
*/
public void setListener(DataControllerListener listener) {
this.listener = listener;
}
/**
* Восстанавливает данные из БД асинхронно
*/
public void restoreDataAsync() {
Log.i(TAG, "🔄 Запускаем асинхронное восстановление данных из БД...");
executor.execute(() -> {
try {
Log.d(TAG, "📊 Загружаем данные судна из БД...");
VesselEntity latest = repository.getLatestOwnVesselSync();
Vessel vessel = null;
if (latest != null) {
vessel = new Vessel();
vessel.setLatitude(latest.latitude);
vessel.setLongitude(latest.longitude);
vessel.setAccuracy(latest.accuracy);
vessel.setFixTime(latest.fixTime);
Log.d(TAG, "✅ Данные судна восстановлены: " + latest.latitude + "," + latest.longitude);
} else {
Log.d(TAG, "ℹ️ Нет данных судна в БД");
}
Log.d(TAG, "🚢 Загружаем AIS суда из БД...");
List<AISVesselEntity> list = repository.getAllAISSync();
List<AISVessel> aisVessels = new ArrayList<>();
if (list != null && !list.isEmpty()) {
for (AISVesselEntity entity : list) {
// Используем маппер для полного восстановления всех полей
AISVessel vesselModel = AISVesselMapper.toModel(entity);
aisVessels.add(vesselModel);
Log.d(TAG, "AIS судно восстановлено из БД с полными данными: " + vesselModel.getMmsi());
}
Log.i(TAG, "✅ Восстановлено " + list.size() + " AIS судов из БД с полными данными");
} else {
Log.d(TAG, "ℹ️ Нет AIS судов в БД");
}
// Уведомляем слушателя о восстановленных данных
if (listener != null) {
listener.onDataRestored(vessel, aisVessels);
}
} catch (Exception e) {
Log.e(TAG, "❌ Ошибка восстановления данных из БД: " + e.getMessage(), e);
}
});
}
/**
* Сохраняет позицию судна в БД
*/
public void saveVesselPosition(Vessel vessel) {
if (vessel == null) return;
executor.execute(() -> {
try {
VesselEntity ve = new VesselEntity();
ve.latitude = vessel.getLatitude();
ve.longitude = vessel.getLongitude();
ve.accuracy = vessel.getAccuracy();
ve.fixTime = vessel.getFixTime();
repository.upsertOwnVessel(ve);
if (listener != null) {
listener.onDataSaved("vessel_position", true);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка сохранения позиции в БД: " + e.getMessage(), e);
if (listener != null) {
listener.onDataSaved("vessel_position", false);
}
}
});
}
/**
* Сохраняет AIS судно в БД
*/
public void saveAISVessel(AISVessel vessel) {
if (vessel == null) return;
executor.execute(() -> {
try {
// Используем маппер для полной конвертации всех полей
AISVesselEntity entity = AISVesselMapper.toEntity(vessel);
repository.upsertAIS(entity);
Log.d(TAG, "AIS судно сохранено в БД с полными данными: " + vessel.getMmsi());
if (listener != null) {
listener.onDataSaved("ais_vessel", true);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
if (listener != null) {
listener.onDataSaved("ais_vessel", false);
}
}
});
}
/**
* Запускает периодическую очистку БД от устаревших 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 {
int staleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes();
long thresholdEpochMs = System.currentTimeMillis() - (staleRemoveMinutes * 60 * 1000L);
repository.deleteStaleAIS(thresholdEpochMs);
Log.i(TAG, "Выполнена очистка БД от AIS целей старше " + staleRemoveMinutes + " минут");
if (listener != null) {
listener.onDataCleaned(0); // Метод не возвращает количество удаленных записей
}
// Планируем следующую очистку
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка при очистке БД от устаревших AIS целей: " + e.getMessage(), e);
}
}
/**
* Получает Repository для прямого доступа (если необходимо)
*/
public Repository getRepository() {
return repository;
}
/**
* Получает SettingsManager для прямого доступа (если необходимо)
*/
public SettingsManager getSettingsManager() {
return settingsManager;
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopDatabaseCleanup();
// Очищаем Handler для предотвращения утечек памяти
if (dbCleanupHandler != null) {
dbCleanupHandler.removeCallbacksAndMessages(null);
}
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
try {
// Ждем завершения всех задач максимум 2 секунды
if (!executor.awaitTermination(2, java.util.concurrent.TimeUnit.SECONDS)) {
Log.w(TAG, "Thread pool did not terminate gracefully, forcing shutdown");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Log.w(TAG, "Thread pool shutdown interrupted: " + e.getMessage());
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Log.i(TAG, "Data Controller очищен");
}
}
@@ -0,0 +1,21 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
/**
* Базовая фабрика: создаёт AppCoordinator, который сам инициализирует контроллеры
*/
public class DefaultControllersFactory implements ControllersFactory {
private static final String TAG = "ControllersFactory";
@Override
public AppCoordinator createAppCoordinator(Context context) {
AppCoordinator appCoordinator = new AppCoordinator(context);
Log.i(TAG, "AppCoordinator создан через фабрику");
return appCoordinator;
}
}
@@ -3,13 +3,20 @@ package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
import com.grigowashere.aismap.maps.MapInterface;
import com.grigowashere.aismap.maps.MapInterfaceChangeListener;
import com.grigowashere.aismap.maps.MarkerManager;
import com.grigowashere.aismap.maps.YandexMapImpl;
import com.grigowashere.aismap.maps.MapLibreMapImpl;
import com.grigowashere.aismap.maps.YandexMarkerManager;
import com.yandex.mapkit.mapview.MapView;
import java.util.ArrayList;
import java.util.List;
/**
* Контроллер для управления картами
* Инкапсулирует логику инициализации и управления различными картами
* Единственный владелец MapInterface - централизованное управление картами
* Уведомляет всех подписчиков о смене стратегии карты
*/
public class MapController {
@@ -21,25 +28,77 @@ public class MapController {
private MapView mapView;
private org.maplibre.android.maps.MapView mapLibreView;
// Менеджер маркеров (SDK-специфичный)
private MarkerManager markerManager;
// Список слушателей изменений MapInterface
private final List<MapInterfaceChangeListener> listeners = new ArrayList<>();
public MapController(Context context) {
this.context = context;
}
/**
* Добавляет слушателя изменений MapInterface
*/
public void addMapInterfaceChangeListener(MapInterfaceChangeListener listener) {
if (listener != null && !listeners.contains(listener)) {
listeners.add(listener);
Log.d(TAG, "Добавлен слушатель изменений MapInterface: " + listener.getClass().getSimpleName());
}
}
/**
* Удаляет слушателя изменений MapInterface
*/
public void removeMapInterfaceChangeListener(MapInterfaceChangeListener listener) {
if (listeners.remove(listener)) {
Log.d(TAG, "Удален слушатель изменений MapInterface: " + listener.getClass().getSimpleName());
}
}
/**
* Уведомляет всех слушателей о смене MapInterface
*/
private void notifyMapInterfaceChanged(MapInterface oldMapInterface, MapInterface newMapInterface) {
Log.i(TAG, "Уведомляем " + listeners.size() + " слушателей о смене MapInterface");
for (MapInterfaceChangeListener listener : listeners) {
try {
listener.onMapInterfaceChanged(oldMapInterface, newMapInterface);
} catch (Exception e) {
Log.e(TAG, "Ошибка при уведомлении слушателя " + listener.getClass().getSimpleName() + ": " + e.getMessage());
}
}
}
/**
* Инициализирует карту указанного типа
*/
public MapInterface initializeMap(String mapType, MapView mapView) {
this.mapView = mapView;
MapInterface oldMapInterface = currentMapInterface;
MapInterface newMapInterface = null;
switch (mapType.toLowerCase()) {
case "yandex":
return initializeYandexMaps();
newMapInterface = initializeYandexMaps();
break;
case "mapforge":
return initializeMapForge();
newMapInterface = initializeMapForge();
break;
default:
Log.e(TAG, "Неизвестный тип карты: " + mapType);
return null;
}
if (newMapInterface != null) {
currentMapInterface = newMapInterface;
initializeMarkerManager(); // Инициализируем MarkerManager
notifyMapInterfaceChanged(oldMapInterface, newMapInterface);
}
return newMapInterface;
}
/**
@@ -49,7 +108,16 @@ public class MapController {
try {
this.mapLibreView = mapLibreView;
Log.i(TAG, "Создаем интерфейс для MapLibre");
MapInterface oldMapInterface = currentMapInterface;
currentMapInterface = new MapLibreMapImpl(context, mapLibreView);
// Инициализируем MarkerManager
initializeMarkerManager();
// Уведомляем слушателей о смене MapInterface
notifyMapInterfaceChanged(oldMapInterface, currentMapInterface);
return currentMapInterface;
} catch (Exception e) {
Log.e(TAG, "Ошибка при создании интерфейса MapLibre: " + e.getMessage());
@@ -67,13 +135,48 @@ public class MapController {
}
}
// AppController удален. Метод оставлен неактивным для обратной совместимости.
/**
* Устанавливает AppController в текущий интерфейс карты
* Устанавливает AppCoordinator в текущий интерфейс карты
*/
public void setAppController(AppController appController) {
public void setAppCoordinator(AppCoordinator appCoordinator) {
if (currentMapInterface instanceof MapLibreMapImpl) {
((MapLibreMapImpl) currentMapInterface).setAppController(appController);
Log.i(TAG, "AppController установлен в MapLibreMapImpl");
((MapLibreMapImpl) currentMapInterface).setAppCoordinator(appCoordinator);
Log.i(TAG, "AppCoordinator установлен в MapLibreMapImpl");
}
}
/**
* Получает текущий MarkerManager
*/
public MarkerManager getMarkerManager() {
return markerManager;
}
/**
* Инициализирует MarkerManager для текущей карты
*/
private void initializeMarkerManager() {
if (currentMapInterface instanceof YandexMapImpl) {
// Для Yandex Maps создаем YandexMarkerManager
markerManager = new YandexMarkerManager(context, null, mapView, null);
Log.i(TAG, "YandexMarkerManager инициализирован");
} else if (currentMapInterface instanceof MapLibreMapImpl) {
// Для MapLibre пока что null - маркеры управляются через MapInterface
markerManager = null;
Log.i(TAG, "MapLibre использует встроенное управление маркерами");
}
}
/**
* Очищает MarkerManager
*/
private void cleanupMarkerManager() {
if (markerManager != null) {
markerManager.cleanup();
markerManager = null;
Log.i(TAG, "MarkerManager очищен");
}
}
@@ -91,8 +194,7 @@ public class MapController {
Log.i(TAG, "Создаем интерфейс для Яндекс.Карт");
// Создаем интерфейс для Яндекс.Карт
currentMapInterface = new YandexMapImpl(context, mapView);
return currentMapInterface;
return new YandexMapImpl(context, mapView);
} catch (Exception e) {
Log.e(TAG, "Ошибка при создании интерфейса Яндекс.Карт: " + e.getMessage());
@@ -140,11 +242,19 @@ public class MapController {
/**
* Получает текущий интерфейс карты
* Единственный способ получить MapInterface извне
*/
public MapInterface getCurrentMapInterface() {
return currentMapInterface;
}
/**
* Получает текущий интерфейс карты (алиас для совместимости)
*/
public MapInterface getMapInterface() {
return getCurrentMapInterface();
}
/**
* Устанавливает флаг инициализации Яндекс.Карт
*/
@@ -156,15 +266,28 @@ public class MapController {
* Освобождает ресурсы
*/
public void cleanup() {
Log.i(TAG, "Очистка MapController");
// Очищаем текущий MapInterface
if (currentMapInterface != null) {
currentMapInterface.cleanup();
currentMapInterface = null;
}
// Очищаем MarkerManager
cleanupMarkerManager();
// Очищаем слушателей
listeners.clear();
// Останавливаем карты
if (mapView != null) { mapView.onStop(); }
if (mapLibreView != null) { mapLibreView.onStop(); }
if (isYandexMapsInitialized) {
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
}
Log.i(TAG, "MapController очищен");
}
}
@@ -0,0 +1,390 @@
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.controllers.NMEAParser;
import com.grigowashere.aismap.controllers.AndroidNMEAListener;
import com.grigowashere.aismap.controllers.GPSLocationListener;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Контроллер для обработки NMEA сообщений
* Отвечает только за парсинг и обработку NMEA данных
*/
public class NMEAController implements
NMEAParser.NMEAParserListener,
AndroidNMEAListener.NMEAMessageCallback,
GPSLocationListener.LocationCallback {
private static final String TAG = "NMEAController";
private Context context;
private NMEAParser nmeaParser;
private AndroidNMEAListener androidNmeaListener;
private GPSLocationListener gpsLocationListener;
private ExecutorService executor;
// Callbacks для уведомления других компонентов
private NMEAControllerListener listener;
// Текущий режим работы
private String currentDataMode = "hybrid";
// Диагностика
private long lastServiceLogTime = 0;
public interface NMEAControllerListener {
void onVesselUpdated(Vessel vessel);
void onDOPUpdated(double pdop, double hdop, double vdop);
void onAISVesselUpdated(AISVessel vessel);
void onParseError(String error);
void onGPSLocationUpdated(Vessel vessel);
}
public NMEAController(Context context) {
this.context = context;
this.executor = Executors.newCachedThreadPool();
initializeParser();
}
/**
* Инициализирует NMEA парсер
*/
private void initializeParser() {
// Инициализация парсера NMEA
nmeaParser = new NMEAParser();
nmeaParser.setListener(this);
// Инициализация GPS Location Listener (для координат)
gpsLocationListener = new GPSLocationListener(context);
gpsLocationListener.setCallback(this);
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
androidNmeaListener = new AndroidNMEAListener(context);
androidNmeaListener.setCallback(this);
Log.i(TAG, "NMEA Controller инициализирован");
}
/**
* Настраивает режим работы на основе настроек
*/
private void configureMode(String dataMode) {
this.currentDataMode = dataMode;
Log.i(TAG, "Настройка режима: " + dataMode);
if ("hybrid".equals(dataMode)) {
// Гибридный режим: GPS для координат, NMEA для остального
nmeaParser.setGPSLocationListener(gpsLocationListener);
nmeaParser.setHybridMode(true);
Log.i(TAG, "Гибридный режим: GPS координаты + NMEA данные");
} else if ("android_only".equals(dataMode)) {
// Только Android GPS - не используем NMEA парсер
nmeaParser.setGPSLocationListener(null);
nmeaParser.setHybridMode(false);
Log.i(TAG, "Режим Android GPS: только встроенный GPS");
} else if ("nmea_only".equals(dataMode)) {
// Только NMEA - игнорируем GPS Location Manager
nmeaParser.setGPSLocationListener(null);
nmeaParser.setHybridMode(false);
Log.i(TAG, "Режим NMEA: только внешний NMEA");
} else {
// По умолчанию гибридный режим
nmeaParser.setGPSLocationListener(gpsLocationListener);
nmeaParser.setHybridMode(true);
Log.i(TAG, "Режим по умолчанию: гибридный");
}
}
/**
* Устанавливает слушателя для уведомлений
*/
public void setListener(NMEAControllerListener listener) {
this.listener = listener;
}
/**
* Запускает Android NMEA слушатель
*/
public void startAndroidNMEAListener() {
if (androidNmeaListener != null && !androidNmeaListener.isListening()) {
Log.i(TAG, "🚀 Запускаем Android NMEA слушатель...");
boolean success = androidNmeaListener.startListening();
if (success) {
Log.i(TAG, "✅ Android NMEA слушатель успешно запущен");
} else {
Log.e(TAG, "❌ Не удалось запустить Android NMEA слушатель");
}
}
}
/**
* Останавливает Android NMEA слушатель
*/
public void stopAndroidNMEAListener() {
if (androidNmeaListener != null && androidNmeaListener.isListening()) {
Log.i(TAG, "⏹️ Останавливаем Android NMEA слушатель...");
androidNmeaListener.stopListening();
}
}
/**
* Запускает GPS Location слушатель
*/
public void startGPSLocationListener() {
if (gpsLocationListener != null && !gpsLocationListener.isListening()) {
Log.i(TAG, "🚀 Запускаем GPS Location слушатель...");
boolean success = gpsLocationListener.startListening();
if (success) {
Log.i(TAG, "✅ GPS Location слушатель успешно запущен");
} else {
Log.e(TAG, "❌ Не удалось запустить GPS Location слушатель");
}
}
}
/**
* Останавливает GPS Location слушатель
*/
public void stopGPSLocationListener() {
if (gpsLocationListener != null && gpsLocationListener.isListening()) {
Log.i(TAG, "⏹️ Останавливаем GPS Location слушатель...");
gpsLocationListener.stopListening();
}
}
/**
* Парсит NMEA сообщение
*/
public void parseNMEAMessage(String message) {
// Фильтруем сообщения в зависимости от режима
if ("android_only".equals(currentDataMode)) {
// В режиме только Android GPS игнорируем GPS NMEA сообщения, но пропускаем AIS
if (isGPSNMEAMessage(message)) {
Log.d(TAG, "Игнорируем GPS NMEA сообщение в режиме android_only: " + message.substring(0, Math.min(20, message.length())));
return;
}
// AIS сообщения (!) пропускаем всегда
}
if (executor != null && !executor.isShutdown()) {
try {
executor.execute(() -> {
try {
nmeaParser.parseNMEA(message);
// Диагностика: логируем каждые 10 секунд
long now = System.currentTimeMillis();
if (now - lastServiceLogTime > 10000) {
Log.d(TAG, "✅ NMEA сообщение обработано в фоновом потоке");
lastServiceLogTime = now;
}
} catch (Exception e) {
Log.e(TAG, "❌ Ошибка парсинга NMEA в фоновом потоке: " + e.getMessage(), e);
if (listener != null) {
listener.onParseError(e.getMessage());
}
}
});
} catch (java.util.concurrent.RejectedExecutionException e) {
Log.w(TAG, "Thread pool is shutting down, skipping NMEA processing: " + e.getMessage());
}
} else {
Log.w(TAG, "Thread pool is not available, skipping NMEA processing");
}
}
/**
* Проверяет, является ли сообщение GPS NMEA (а не AIS)
*/
private boolean isGPSNMEAMessage(String message) {
if (message == null || message.length() < 6) {
return false;
}
// AIS сообщения начинаются с "!" - их всегда пропускаем
if (message.startsWith("!")) {
return false;
}
// GPS NMEA сообщения начинаются с "$" - их фильтруем в режиме android_only
if (message.startsWith("$")) {
return true;
}
// Остальные сообщения пропускаем
return false;
}
/**
* Устанавливает режим работы с данными
*/
public void setDataMode(String mode) {
String oldMode = currentDataMode;
configureMode(mode);
// Если режим изменился, перезапускаем слушатели
if (!oldMode.equals(mode)) {
Log.i(TAG, "Режим изменился с " + oldMode + " на " + mode + ", перезапускаем слушатели");
restartListeners();
}
}
/**
* Перезапускает слушатели на основе текущего режима
*/
private void restartListeners() {
Log.i(TAG, "🔄 Перезапускаем слушатели в режиме: " + currentDataMode);
// Останавливаем все слушатели
stopAndroidNMEAListener();
stopGPSLocationListener();
// Запускаем нужные слушатели в зависимости от режима
if ("hybrid".equals(currentDataMode)) {
Log.i(TAG, "📍 Гибридный режим: Android GPS + NMEA");
startAndroidNMEAListener();
startGPSLocationListener();
} else if ("android_only".equals(currentDataMode)) {
Log.i(TAG, "📍 Режим Android GPS: только встроенный GPS");
startGPSLocationListener();
} else if ("nmea_only".equals(currentDataMode)) {
Log.i(TAG, "📍 Режим NMEA: только внешний NMEA");
startAndroidNMEAListener();
} else {
Log.i(TAG, "📍 Режим по умолчанию: гибридный");
startAndroidNMEAListener();
startGPSLocationListener();
}
Log.i(TAG, "✅ Слушатели перезапущены");
}
/**
* Получает GPS Location Listener для внешнего использования
*/
public GPSLocationListener getGPSLocationListener() {
return gpsLocationListener;
}
/**
* Проверяет, включен ли Android NMEA слушатель
*/
public boolean isAndroidNMEAListenerActive() {
return androidNmeaListener != null && androidNmeaListener.isListening();
}
/**
* Проверяет, включен ли GPS Location слушатель
*/
public boolean isGPSLocationListenerActive() {
return gpsLocationListener != null && gpsLocationListener.isListening();
}
// Реализация NMEAParserListener
@Override
public void onVesselUpdated(Vessel vessel) {
if (listener != null) {
listener.onVesselUpdated(vessel);
}
}
@Override
public void onDOPUpdated(double pdop, double hdop, double vdop) {
if (listener != null) {
listener.onDOPUpdated(pdop, hdop, vdop);
}
}
@Override
public void onAISVesselUpdated(AISVessel vessel) {
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
}
@Override
public void onParseError(String error) {
Log.e(TAG, "Ошибка парсинга NMEA: " + error);
if (listener != null) {
listener.onParseError(error);
}
}
// Реализация NMEAMessageCallback
@Override
public void onNMEAMessage(String message, long timestamp) {
// Диагностика: логируем каждые 10 секунд
long now = System.currentTimeMillis();
if (now - lastServiceLogTime > 10000) {
Log.d(TAG, "📱 Android NMEA сообщение получено");
lastServiceLogTime = now;
}
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
parseNMEAMessage(message);
}
@Override
public void onGPSStatusChanged(int status) {
Log.i(TAG, "GPS статус изменился: " + status);
}
// Реализация GPSLocationListener.LocationCallback
@Override
public void onLocationUpdated(Vessel vessel) {
// Игнорируем обновления от GPS, если режим только NMEA
if ("nmea_only".equals(currentDataMode)) {
Log.d(TAG, "Игнорируем GPS обновление в режиме nmea_only");
return;
}
if (listener != null) {
listener.onGPSLocationUpdated(vessel);
}
}
@Override
public void onError(String error) {
Log.e(TAG, "Ошибка Android NMEA: " + error);
if (listener != null) {
listener.onParseError(error);
}
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopAndroidNMEAListener();
stopGPSLocationListener();
if (androidNmeaListener != null) {
androidNmeaListener.cleanup();
}
if (gpsLocationListener != null) {
gpsLocationListener.cleanup();
}
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
try {
// Ждем завершения всех задач максимум 2 секунды
if (!executor.awaitTermination(2, java.util.concurrent.TimeUnit.SECONDS)) {
Log.w(TAG, "Thread pool did not terminate gracefully, forcing shutdown");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Log.w(TAG, "Thread pool shutdown interrupted: " + e.getMessage());
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Log.i(TAG, "NMEA Controller очищен");
}
}
@@ -0,0 +1,244 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
import com.grigowashere.aismap.controllers.UDPListener;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Контроллер для сетевых операций
* Отвечает только за UDP слушание и отправку данных
*/
public class NetworkController implements UDPListener.UDPListenerCallback {
private static final String TAG = "NetworkController";
private Context context;
private UDPListener udpListener;
private ExecutorService executor;
// Настройки сети
private int udpPort = 10110; // Стандартный порт для AIS
private boolean isUDPEnabled = false;
private boolean isUDPNMEAEnabled = false;
// Callbacks для уведомления других компонентов
private NetworkControllerListener listener;
// Диагностика
private long lastServiceLogTime = 0;
public interface NetworkControllerListener {
void onDataReceived(String data, String sourceAddress, int sourcePort);
void onUDPError(String error);
}
public NetworkController(Context context) {
this.context = context;
this.executor = Executors.newCachedThreadPool();
initializeUDPListener();
}
/**
* Инициализирует UDP слушатель
*/
private void initializeUDPListener() {
udpListener = new UDPListener(udpPort);
udpListener.setCallback(this);
Log.i(TAG, "Network Controller инициализирован с портом: " + udpPort);
}
/**
* Устанавливает слушателя для уведомлений
*/
public void setListener(NetworkControllerListener listener) {
this.listener = listener;
}
/**
* Включает/выключает UDP слушатель
*/
public void setUDPEnabled(boolean enabled) {
this.isUDPEnabled = enabled;
if (enabled && !udpListener.isRunning()) {
startUDPListener();
} else if (!enabled && udpListener.isRunning()) {
stopUDPListener();
}
Log.i(TAG, "UDP слушатель: " + (enabled ? "включен" : "выключен"));
}
/**
* Запускает UDP слушатель
*/
public void startUDPListener() {
if (isUDPEnabled && executor != null && !executor.isShutdown()) {
try {
executor.execute(() -> {
udpListener.start();
Log.i(TAG, "UDP слушатель запущен на порту: " + udpPort);
});
} catch (java.util.concurrent.RejectedExecutionException e) {
Log.w(TAG, "Thread pool is shutting down, cannot start UDP listener: " + e.getMessage());
}
}
}
/**
* Останавливает UDP слушатель
*/
public void stopUDPListener() {
if (udpListener.isRunning()) {
executor.execute(() -> {
udpListener.stop();
Log.i(TAG, "UDP слушатель остановлен");
});
}
}
/**
* Отправляет данные по UDP
*/
public void sendUDPData(String data, String address, int port) {
if (udpListener != null) {
udpListener.sendData(data, address, port);
}
}
/**
* Устанавливает 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 слушателя");
restartUDPListener();
}
}
/**
* Перезапускает 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) {
startUDPListener();
Log.i(TAG, "UDP слушатель перезапущен на порту: " + udpPort);
}
}
}
/**
* Включает/выключает UDP NMEA
*/
public void setUDPNMEAEnabled(boolean enabled) {
this.isUDPNMEAEnabled = enabled;
Log.i(TAG, "UDP NMEA: " + (enabled ? "включен" : "выключен"));
}
/**
* Проверяет, включен ли UDP слушатель
*/
public boolean isUDPEnabled() {
return isUDPEnabled;
}
/**
* Проверяет, включен ли UDP NMEA
*/
public boolean isUDPNMEAEnabled() {
return isUDPNMEAEnabled;
}
/**
* Получает текущий UDP порт
*/
public int getUDPPort() {
return udpPort;
}
/**
* Получает статус сетевых настроек
*/
public String getNetworkStatus() {
return String.format(
"UDP: порт=%d, включен=%s, NMEA=%s",
udpPort,
isUDPEnabled ? "да" : "нет",
isUDPNMEAEnabled ? "включен" : "выключен"
);
}
// Реализация UDPListenerCallback
@Override
public void onDataReceived(String data, String sourceAddress, int sourcePort) {
// Диагностика: логируем каждые 10 секунд
long now = System.currentTimeMillis();
if (now - lastServiceLogTime > 10000) {
Log.d(TAG, "📡 UDP данные получены от " + sourceAddress + ":" + sourcePort);
lastServiceLogTime = now;
}
// Передаем данные слушателю
if (listener != null) {
listener.onDataReceived(data, sourceAddress, sourcePort);
}
}
@Override
public void onUDPError(String error) {
Log.e(TAG, "UDP ошибка: " + error);
if (listener != null) {
listener.onUDPError(error);
}
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopUDPListener();
if (udpListener != null) {
udpListener.cleanup();
}
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
try {
// Ждем завершения всех задач максимум 2 секунды
if (!executor.awaitTermination(2, java.util.concurrent.TimeUnit.SECONDS)) {
Log.w(TAG, "Thread pool did not terminate gracefully, forcing shutdown");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Log.w(TAG, "Thread pool shutdown interrupted: " + e.getMessage());
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
Log.i(TAG, "Network Controller очищен");
}
}
@@ -0,0 +1,168 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
import com.grigowashere.aismap.services.NotificationService;
import com.grigowashere.aismap.models.AISVessel;
/**
* Контроллер для управления уведомлениями
* Отвечает только за показ уведомлений пользователю
*/
public class NotificationController {
private static final String TAG = "NotificationController";
private Context context;
private NotificationService notificationService;
// Callbacks для уведомления других компонентов
private NotificationControllerListener listener;
public interface NotificationControllerListener {
void onNotificationShown(String type, String message);
void onNotificationError(String error);
}
public NotificationController(Context context) {
this.context = context;
this.notificationService = new NotificationService(context);
Log.i(TAG, "Notification Controller инициализирован");
}
/**
* Устанавливает слушателя для уведомлений
*/
public void setListener(NotificationControllerListener listener) {
this.listener = listener;
}
/**
* Показывает уведомление о новой AIS цели
*/
public void notifyNewAISTarget() {
if (notificationService != null && notificationService.areNotificationsEnabled()) {
try {
notificationService.notifyNewAISTarget();
Log.i(TAG, "🔔 Уведомление о новой AIS цели показано");
if (listener != null) {
listener.onNotificationShown("new_ais_target", "Новая AIS цель обнаружена");
}
} catch (Exception e) {
Log.e(TAG, "Ошибка показа уведомления о новой AIS цели: " + e.getMessage(), e);
if (listener != null) {
listener.onNotificationError(e.getMessage());
}
}
} else {
Log.d(TAG, "Уведомления отключены или сервис недоступен");
}
}
/**
* Показывает уведомление о safety-сообщении
*/
public void notifySafetyMessage(String mmsi, String message) {
if (notificationService != null && notificationService.areNotificationsEnabled()) {
try {
notificationService.notifySafetyMessage(mmsi, message);
Log.i(TAG, "🔔 Safety-сообщение показано для MMSI: " + mmsi);
if (listener != null) {
listener.onNotificationShown("safety_message", "Safety-сообщение от " + mmsi);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка показа safety-сообщения: " + e.getMessage(), e);
if (listener != null) {
listener.onNotificationError(e.getMessage());
}
}
} else {
Log.d(TAG, "Уведомления отключены или сервис недоступен");
}
}
/**
* Показывает уведомление о GPS статусе
*/
public void notifyGPSStatus(String status) {
if (notificationService != null && notificationService.areNotificationsEnabled()) {
try {
// Здесь можно добавить специфичную логику для GPS уведомлений
Log.i(TAG, "🔔 GPS статус: " + status);
if (listener != null) {
listener.onNotificationShown("gps_status", "GPS статус: " + status);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка показа GPS уведомления: " + e.getMessage(), e);
if (listener != null) {
listener.onNotificationError(e.getMessage());
}
}
}
}
/**
* Показывает уведомление об ошибке соединения
*/
public void notifyConnectionError(String error) {
if (notificationService != null && notificationService.areNotificationsEnabled()) {
try {
// Здесь можно добавить специфичную логику для уведомлений об ошибках
Log.i(TAG, "🔔 Ошибка соединения: " + error);
if (listener != null) {
listener.onNotificationShown("connection_error", "Ошибка соединения: " + error);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка показа уведомления об ошибке: " + e.getMessage(), e);
if (listener != null) {
listener.onNotificationError(e.getMessage());
}
}
}
}
/**
* Проверяет, включены ли уведомления
*/
public boolean areNotificationsEnabled() {
return notificationService != null && notificationService.areNotificationsEnabled();
}
/**
* Включает/выключает уведомления
*/
public void setNotificationsEnabled(boolean enabled) {
if (notificationService != null) {
notificationService.setNotificationsEnabled(enabled);
Log.i(TAG, "Уведомления: " + (enabled ? "включены" : "выключены"));
}
}
/**
* Получает статус уведомлений
*/
public String getNotificationStatus() {
return "Уведомления: " + (areNotificationsEnabled() ? "включены" : "выключены");
}
/**
* Получает NotificationService для прямого доступа (если необходимо)
*/
public NotificationService getNotificationService() {
return notificationService;
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
if (notificationService != null) {
notificationService.cleanup();
}
Log.i(TAG, "Notification Controller очищен");
}
}
@@ -188,6 +188,16 @@ public class MapForgeImpl implements MapInterface {
// В будущем можно добавить поддержку трекинга пути для MapForge
}
/**
* Обновление всех путей судов на карте (заглушка для MapForge)
*/
@Override
public void updateAllVesselPaths() {
// TODO: Реализовать обновление путей для MapForge
// MapForge не поддерживает трекинг пути в данной реализации
// Пока что это заглушка
}
private org.mapsforge.core.graphics.Bitmap createMapForgeIcon(int color, double course) {
// Создаем простую иконку для MapForge
// В реальном приложении нужно конвертировать Android Bitmap в MapForge Bitmap
@@ -94,6 +94,11 @@ public interface MapInterface {
*/
void clearVesselPath();
/**
* Обновление всех путей судов на карте
*/
void updateAllVesselPaths();
/**
* Показать курсор на карте
*/
@@ -0,0 +1,15 @@
package com.grigowashere.aismap.maps;
/**
* Интерфейс для уведомления о смене MapInterface
* Используется для синхронизации всех компонентов при смене стратегии карты
*/
public interface MapInterfaceChangeListener {
/**
* Вызывается при смене MapInterface
* @param oldMapInterface предыдущий интерфейс карты (может быть null)
* @param newMapInterface новый интерфейс карты
*/
void onMapInterfaceChanged(MapInterface oldMapInterface, MapInterface newMapInterface);
}
@@ -12,13 +12,15 @@ import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.utils.SettingsManager;
import com.grigowashere.aismap.utils.GeoUtils;
import com.grigowashere.aismap.controllers.VesselPathController;
import com.grigowashere.aismap.controllers.AppController;
import com.grigowashere.aismap.controllers.AppCoordinator;
import com.grigowashere.aismap.view.CursorOverlay;
import com.grigowashere.aismap.R;
import android.view.ViewGroup;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.List;
import org.maplibre.android.maps.MapView;
import org.maplibre.android.maps.MapLibreMap;
import org.maplibre.android.maps.Style;
@@ -79,7 +81,8 @@ public class MapLibreMapImpl implements MapInterface {
private Style style;
private final SettingsManager settingsManager;
private VesselPathController pathController;
private AppController appController; // Для доступа к AIS VesselPathController
// AppController удален; используем AppCoordinator для доступа к данным
private AppCoordinator appCoordinator; // Новый координатор для доступа к AIS судам
private CursorOverlay cursorOverlay;
private Vessel ownVessel;
@@ -138,9 +141,10 @@ public class MapLibreMapImpl implements MapInterface {
idToFeature.remove(m);
idToAisVessel.remove(m);
// Очищаем путь AIS судна через AppController
if (appController != null) {
appController.clearAISVesselPath(m);
// Очищаем путь AIS судна через AppCoordinator
if (appCoordinator != null) {
VesselPathController c = appCoordinator.getAISVesselPathController(m);
if (c != null) c.clearPath();
}
// Очищаем путь и прогноз AIS судна
@@ -174,7 +178,7 @@ public class MapLibreMapImpl implements MapInterface {
private Vessel lastOwnVessel;
// Буфер координат пути собственного судна
private final JSONArray ownPathCoords = new JSONArray();
// Буферы координат путей AIS судов больше не нужны - используем VesselPathController из AppController
// Буферы координат путей AIS судов больше не нужны - используем VesselPathController из AppCoordinator
// Хранилище FeatureCollection для путей и прогнозов AIS судов
private final Map<String, JSONObject> aisPathFeatures = new HashMap<>();
@@ -211,12 +215,14 @@ public class MapLibreMapImpl implements MapInterface {
Log.i(TAG, "VesselPathController установлен в MapLibreMapImpl");
}
// setAppController удален
/**
* Устанавливает AppController для доступа к AIS VesselPathController
* Устанавливает AppCoordinator для доступа к AIS судам
*/
public void setAppController(AppController appController) {
this.appController = appController;
Log.i(TAG, "AppController установлен в MapLibreMapImpl");
public void setAppCoordinator(AppCoordinator appCoordinator) {
this.appCoordinator = appCoordinator;
Log.i(TAG, "AppCoordinator установлен в MapLibreMapImpl");
}
/**
@@ -418,7 +424,7 @@ public class MapLibreMapImpl implements MapInterface {
// Добавляем трассировку пути и предсказание для AIS судна
if (PATH_FEATURES_ENABLED) {
Log.d(TAG, "PATH_FEATURES_ENABLED=true, updating AIS path and prediction for " + vessel.getMmsi());
// Переносим обновление AIS пути на UI поток (теперь используем VesselPathController из AppController)
// Переносим обновление AIS пути на UI поток (используем VesselPathController из AppCoordinator)
uiHandler.post(() -> updateAISPathSource(vessel.getMmsi()));
// Переносим обновление AIS прогноза на UI поток
uiHandler.post(() -> updateAISVesselPredictionSource(vessel.getMmsi(), vessel));
@@ -463,9 +469,12 @@ public class MapLibreMapImpl implements MapInterface {
idToFeature.remove(mmsi);
idToAisVessel.remove(mmsi);
// Очищаем путь AIS судна через AppController
if (appController != null) {
appController.clearAISVesselPath(mmsi);
// Очищаем путь AIS судна через AppCoordinator
if (appCoordinator != null) {
try {
VesselPathController c = appCoordinator.getAISVesselPathController(mmsi);
if (c != null) c.clearPath();
} catch (Exception ignore) {}
}
// Очищаем путь и прогноз AIS судна на UI потоке
@@ -484,9 +493,17 @@ public class MapLibreMapImpl implements MapInterface {
idToFeature.entrySet().removeIf(e -> !"own_vessel".equals(e.getKey()));
idToAisVessel.clear();
// Очищаем все пути AIS судов через AppController
if (appController != null) {
appController.clearAllAISVesselPaths();
// Очищаем все пути AIS судов через AppCoordinator
if (appCoordinator != null) {
try {
java.util.List<AISVessel> list = appCoordinator.getAISVessels();
if (list != null) {
for (AISVessel v : list) {
VesselPathController c = appCoordinator.getAISVesselPathController(v.getMmsi());
if (c != null) c.clearPath();
}
}
} catch (Exception ignore) {}
}
// Очищаем все пути и прогнозы AIS судов на UI потоке
@@ -1237,10 +1254,12 @@ public class MapLibreMapImpl implements MapInterface {
return;
}
// Получаем VesselPathController для этого AIS судна из AppController
// Получаем VesselPathController для этого AIS судна из AppCoordinator
VesselPathController aisPathController = null;
if (appController != null) {
aisPathController = appController.getAISVesselPathController(mmsi);
if (appCoordinator != null) {
try {
aisPathController = appCoordinator.getAISVesselPathController(mmsi);
} catch (Exception ignore) {}
}
if (aisPathController == null) {
@@ -1538,6 +1557,53 @@ public class MapLibreMapImpl implements MapInterface {
}
}
/**
* Обновляет источник пути для AIS судна
*/
private void updateAISVesselPathSource(String mmsi, JSONArray pathCoords) {
try {
if (maplibreMap == null || maplibreMap.getStyle() == null) {
Log.w(TAG, "MapLibre map или style is null, cannot update AIS path source");
return;
}
Style style = maplibreMap.getStyle();
String sourceId = "ais_paths_source";
GeoJsonSource src = (GeoJsonSource) style.getSource(sourceId);
if (src == null) {
Log.w(TAG, "Источник путей AIS не найден!");
return;
}
if (pathCoords.length() < 2) {
Log.d(TAG, "Недостаточно точек пути для AIS судна " + mmsi + ": " + pathCoords.length() + " (нужно минимум 2)");
return;
}
// Создаем LineString для пути AIS судна
JSONObject feature = new JSONObject();
feature.put("type", "Feature");
JSONObject geometry = new JSONObject();
geometry.put("type", "LineString");
geometry.put("coordinates", pathCoords);
feature.put("geometry", geometry);
JSONObject properties = new JSONObject();
properties.put("mmsi", mmsi);
properties.put("vessel_type", "ais");
feature.put("properties", properties);
// Обновляем источник
src.setGeoJson(feature.toString());
Log.d(TAG, "Путь AIS судна " + mmsi + " обновлен: " + pathCoords.length() + " точек");
} catch (Exception e) {
Log.e(TAG, "Ошибка обновления пути AIS судна " + mmsi + ": " + e.getMessage(), e);
}
}
/**
* Сохраняет текущий путь судна
*/
@@ -1590,6 +1656,70 @@ public class MapLibreMapImpl implements MapInterface {
}
}
/**
* Обновляет все пути судов на карте
*/
@Override
public void updateAllVesselPaths() {
Log.d(TAG, "MapLibreMapImpl.updateAllVesselPaths() вызван");
// Обновляем путь собственного судна
if (pathController != null) {
restoreVesselPath();
}
// Обновляем пути AIS судов
updateAISVesselPaths();
Log.d(TAG, "Все пути судов обновлены");
}
/**
* Обновляет пути AIS судов
*/
private void updateAISVesselPaths() {
if (appCoordinator == null) {
Log.w(TAG, "appCoordinator is null, не можем обновить пути AIS судов");
return;
}
try {
// Получаем все AIS суда
List<AISVessel> aisVessels = appCoordinator.getAISVessels();
Log.d(TAG, "Обновляем пути для " + aisVessels.size() + " AIS судов");
for (AISVessel vessel : aisVessels) {
String mmsi = vessel.getMmsi();
VesselPathController aisPathController = appCoordinator.getAISVesselPathController(mmsi);
if (aisPathController != null) {
// Обновляем путь этого AIS судна
updateAISVesselPath(mmsi, aisPathController);
}
}
} catch (Exception e) {
Log.e(TAG, "Ошибка обновления путей AIS судов: " + e.getMessage(), e);
}
}
/**
* Обновляет путь конкретного AIS судна
*/
private void updateAISVesselPath(String mmsi, VesselPathController pathController) {
try {
// Получаем координаты пути из VesselPathController
JSONArray pathCoords = pathController.getPathCoordinates();
if (pathCoords != null && pathCoords.length() >= 2) {
// Обновляем источник на карте
updateAISVesselPathSource(mmsi, pathCoords);
Log.d(TAG, "Путь AIS судна " + mmsi + " обновлен: " + pathCoords.length() + " точек");
}
} catch (Exception e) {
Log.e(TAG, "Ошибка обновления пути AIS судна " + mmsi + ": " + e.getMessage(), e);
}
}
/**
* Получает информацию о пути судна
*/
@@ -317,6 +317,16 @@ public class YandexMapImpl implements MapInterface {
// но если в будущем будет использоваться, нужно добавить очистку
}
/**
* Обновление всех путей судов на карте (заглушка для Yandex)
*/
@Override
public void updateAllVesselPaths() {
// TODO: Реализовать обновление путей для Yandex Maps
// В Yandex Maps пути судов управляются через YandexMarkerManager
// Пока что это заглушка
}
/**
* Очищает все пути движения
*/
@@ -152,7 +152,15 @@ public class NotificationService {
* Проверяет, включены ли уведомления
*/
public boolean areNotificationsEnabled() {
return settingsManager.isVibrationEnabled() || settingsManager.isSoundEnabled();
return settingsManager.areNotificationsEnabled();
}
/**
* Включает/выключает уведомления
*/
public void setNotificationsEnabled(boolean enabled) {
settingsManager.setNotificationsEnabled(enabled);
Log.i(TAG, "Уведомления: " + (enabled ? "включены" : "выключены"));
}
/**
@@ -0,0 +1,121 @@
package com.grigowashere.aismap.ui;
import android.content.Context;
import android.util.Log;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.R;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TextView;
public class BottomSheetsBinder {
private static final String TAG = "BottomSheetsBinder";
private final Context context;
private BottomSheetDialog ownVesselBottomSheet;
private BottomSheetDialog aisVesselBottomSheet;
private View ownBottomSheetView;
private View aisBottomSheetView;
private AISVessel currentAISVessel;
// Auto update
private android.os.Handler updateHandler;
private Runnable updateRunnable;
private int updateIntervalMs = 1000;
public BottomSheetsBinder(Context context) {
this.context = context;
this.updateHandler = new android.os.Handler(android.os.Looper.getMainLooper());
}
public void setUpdateIntervalMs(int intervalMs) {
this.updateIntervalMs = Math.max(250, intervalMs);
}
public void init(View ownBottomSheetView) {
this.ownBottomSheetView = ownBottomSheetView;
ownVesselBottomSheet = new BottomSheetDialog(context);
ownVesselBottomSheet.setContentView(ownBottomSheetView);
ownVesselBottomSheet.setCanceledOnTouchOutside(true);
ownVesselBottomSheet.setCancelable(true);
}
public void initAIS(View aisView) {
this.aisBottomSheetView = aisView;
aisVesselBottomSheet = new BottomSheetDialog(context);
aisVesselBottomSheet.setContentView(aisBottomSheetView);
aisVesselBottomSheet.setCanceledOnTouchOutside(true);
aisVesselBottomSheet.setCancelable(true);
ImageButton btnCloseAIS = aisBottomSheetView.findViewById(R.id.btn_close_ais_bottom_sheet);
if (btnCloseAIS != null) {
btnCloseAIS.setOnClickListener(v -> aisVesselBottomSheet.dismiss());
}
}
public void showOwnVesselSheet() {
if (ownVesselBottomSheet != null && !ownVesselBottomSheet.isShowing()) {
// Обновление UI делегируется вызывающей стороне при необходимости
ownVesselBottomSheet.show();
startAutoUpdate();
}
}
public void showOwnVesselSheet(Runnable onUpdateStart) {
if (ownVesselBottomSheet != null && !ownVesselBottomSheet.isShowing()) {
if (onUpdateStart != null) onUpdateStart.run();
ownVesselBottomSheet.show();
startAutoUpdate();
}
}
public void showAISVesselSheet(AISVessel vessel) {
this.currentAISVessel = vessel;
if (aisVesselBottomSheet != null && !aisVesselBottomSheet.isShowing() && vessel != null) {
// Обновление UI делегируется вызывающей стороне при необходимости
aisVesselBottomSheet.show();
startAutoUpdate();
}
}
public void showAISVesselSheet(AISVessel vessel, Runnable onUpdateStart) {
this.currentAISVessel = vessel;
if (aisVesselBottomSheet != null && !aisVesselBottomSheet.isShowing() && vessel != null) {
if (onUpdateStart != null) onUpdateStart.run();
aisVesselBottomSheet.show();
startAutoUpdate();
}
}
public void startAutoUpdate() {
if (updateRunnable != null) {
updateHandler.removeCallbacks(updateRunnable);
}
updateRunnable = new Runnable() {
@Override public void run() {
try {
// Обновление контента выполняется стороной-владельцем
} finally {
updateHandler.postDelayed(this, updateIntervalMs);
}
}
};
updateHandler.postDelayed(updateRunnable, updateIntervalMs);
}
public void stopAutoUpdate() {
if (updateRunnable != null) {
updateHandler.removeCallbacks(updateRunnable);
}
}
public void updateOwnVesselUI() { /* делегируется владельцу */ }
public void updateAISUI(AISVessel vessel) { /* делегируется владельцу */ }
public void updateAISTimeAgo() { /* делегируется владельцу */ }
private String safe(String s) { return s==null?"--":s; }
}
@@ -0,0 +1,311 @@
package com.grigowashere.aismap.ui;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ImageButton;
import android.widget.TextView;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.grigowashere.aismap.R;
import com.grigowashere.aismap.controllers.AppCoordinator;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.Vessel;
/**
* Полный менеджер BottomSheet-ов: создание, показ, обновление и авто-обновление.
* Делегирует всю прежнюю логику из MainActivity, чтобы разгрузить активити.
*/
public class BottomSheetsManager {
private static final String TAG = "BottomSheetsManager";
private final Context context;
private final AppCoordinator appCoordinator;
private BottomSheetDialog ownVesselBottomSheet;
private View bottomSheetView;
private BottomSheetDialog aisVesselBottomSheet;
private View aisBottomSheetView;
private AISVessel currentAISVessel;
private android.os.Handler timeUpdateHandler;
private Runnable timeUpdateRunnable;
private android.os.Handler bottomSheetUpdateHandler;
private Runnable bottomSheetUpdateRunnable;
private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000;
public BottomSheetsManager(Context context, AppCoordinator appCoordinator) {
this.context = context;
this.appCoordinator = appCoordinator;
}
public void init() {
// Время
timeUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper());
timeUpdateRunnable = new Runnable() {
@Override public void run() {
if (currentAISVessel != null && aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing()) {
updateAISTimeAgo();
}
timeUpdateHandler.postDelayed(this, 1000);
}
};
// Авто-обновление контента
bottomSheetUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper());
bottomSheetUpdateRunnable = new Runnable() {
@Override public void run() {
if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) {
updateOwnVesselUI();
}
if (aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing() && currentAISVessel != null) {
updateAISBottomSheetUI(currentAISVessel);
}
bottomSheetUpdateHandler.postDelayed(this, BOTTOM_SHEET_UPDATE_INTERVAL);
}
};
// Наше судно
ownVesselBottomSheet = new BottomSheetDialog(context);
bottomSheetView = android.view.LayoutInflater.from(context).inflate(R.layout.bottom_sheet_own_vessel, null);
ownVesselBottomSheet.setContentView(bottomSheetView);
ImageButton btnClose = bottomSheetView.findViewById(R.id.btn_close_bottom_sheet);
if (btnClose != null) {
btnClose.setOnClickListener(v -> {
ownVesselBottomSheet.dismiss();
stopBottomSheetAutoUpdate();
});
}
ownVesselBottomSheet.setCanceledOnTouchOutside(true);
ownVesselBottomSheet.setCancelable(true);
ownVesselBottomSheet.setOnDismissListener(dialog -> stopBottomSheetAutoUpdate());
// AIS судно
aisVesselBottomSheet = new BottomSheetDialog(context);
aisBottomSheetView = android.view.LayoutInflater.from(context).inflate(R.layout.bottom_sheet_ais_vessel, null);
aisVesselBottomSheet.setContentView(aisBottomSheetView);
ImageButton btnCloseAIS = aisBottomSheetView.findViewById(R.id.btn_close_ais_bottom_sheet);
if (btnCloseAIS != null) {
btnCloseAIS.setOnClickListener(v -> {
aisVesselBottomSheet.dismiss();
stopTimeUpdate();
stopBottomSheetAutoUpdate();
});
}
aisVesselBottomSheet.setCanceledOnTouchOutside(true);
aisVesselBottomSheet.setCancelable(true);
aisVesselBottomSheet.setOnDismissListener(dialog -> {
stopTimeUpdate();
stopBottomSheetAutoUpdate();
});
}
public void showOwnVessel() {
if (ownVesselBottomSheet != null && !ownVesselBottomSheet.isShowing()) {
updateOwnVesselUI();
ownVesselBottomSheet.show();
startBottomSheetAutoUpdate();
}
}
public void showAISVessel(AISVessel vessel) {
if (aisVesselBottomSheet != null && !aisVesselBottomSheet.isShowing()) {
currentAISVessel = vessel;
updateAISBottomSheetUI(vessel);
aisVesselBottomSheet.show();
startTimeUpdate();
startBottomSheetAutoUpdate();
}
}
public void updateOwnVesselUI() {
if (bottomSheetView == null) return;
Vessel vessel = appCoordinator.getOwnVessel();
if (vessel == null) return;
TextView tvStatus = bottomSheetView.findViewById(R.id.bottom_sheet_status);
TextView tvPosition = bottomSheetView.findViewById(R.id.bottom_sheet_position);
TextView tvCourse = bottomSheetView.findViewById(R.id.bottom_sheet_course);
TextView tvSpeed = bottomSheetView.findViewById(R.id.bottom_sheet_speed);
TextView tvAltitude = bottomSheetView.findViewById(R.id.bottom_sheet_altitude);
TextView tvAccuracy = bottomSheetView.findViewById(R.id.bottom_sheet_accuracy);
TextView tvGPSQuality = bottomSheetView.findViewById(R.id.bottom_sheet_gps_quality);
TextView tvSatellites = bottomSheetView.findViewById(R.id.bottom_sheet_satellites);
TextView tvDOP = bottomSheetView.findViewById(R.id.bottom_sheet_dop);
TextView tvFixTime = bottomSheetView.findViewById(R.id.bottom_sheet_fix_time);
TextView tvFixQuality = bottomSheetView.findViewById(R.id.bottom_sheet_fix_quality);
if (tvStatus != null) tvStatus.setText((vessel.getLatitude()!=0 && vessel.getLongitude()!=0) ? "Статус: GPS активен, данные получены" : "Статус: Ожидание GPS данных...");
if (tvPosition != null) tvPosition.setText((vessel.getLatitude()!=0 && vessel.getLongitude()!=0) ? String.format("📍 Координаты: %.6f, %.6f", vessel.getLatitude(), vessel.getLongitude()) : "📍 Координаты: Не определены");
if (tvCourse != null) tvCourse.setText((vessel.getCourse()>0) ? String.format("🧭 Курс: %.1f°", vessel.getCourse()) : "🧭 Курс: --°");
if (tvSpeed != null) tvSpeed.setText((vessel.getSpeed()>0) ? String.format("⚡ Скорость: %.1f узлов", vessel.getSpeed()) : "⚡ Скорость: -- узлов");
if (tvAltitude != null) tvAltitude.setText((vessel.getAltitude()!=0) ? String.format("🏔️ Высота: %.1f м", vessel.getAltitude()) : "🏔️ Высота: -- м");
if (tvAccuracy != null) tvAccuracy.setText((vessel.getAccuracy()>0) ? String.format("🎯 Точность: %.1f м", vessel.getAccuracy()) : "🎯 Точность: -- м");
if (tvGPSQuality != null) tvGPSQuality.setText((vessel.getGPSQualityDescription()!=null) ? String.format("📊 Качество GPS: %s", vessel.getGPSQualityDescription()) : "📊 Качество GPS: --");
if (tvSatellites != null) tvSatellites.setText((vessel.getSatellites()>0) ? String.format("Спутники: %d/%d", vessel.getActiveSatellites(), vessel.getSatellites()) : "Спутники: --/--");
if (tvDOP != null) tvDOP.setText((vessel.getPdop()>0) ? String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", vessel.getPdop(), vessel.getHdop(), vessel.getVdop()) : "📈 DOP: PDOP=-- HDOP=-- VDOP=--");
if (tvFixTime != null) tvFixTime.setText((vessel.getFixTime()>0) ? String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(new java.util.Date(vessel.getFixTime()))) : "🕐 Время фикса: --");
if (tvFixQuality != null) tvFixQuality.setText((vessel.getFixQuality()!=null) ? String.format("🔒 Качество фикса: %s", vessel.getFixQuality()) : "🔒 Качество фикса: --");
}
public void updateAISBottomSheetUI(AISVessel vessel) {
if (aisBottomSheetView == null || vessel == null) return;
if (currentAISVessel != null && currentAISVessel.getMmsi() != null && currentAISVessel.getMmsi().equals(vessel.getMmsi())) {
currentAISVessel = vessel;
}
TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title);
TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi);
TextView tvCallsign = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_callsign);
TextView tvImo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_imo);
TextView tvType = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_type);
TextView tvPosition = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_position);
TextView tvCourse = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_course);
TextView tvRot = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_rot);
TextView tvHeading = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_heading);
TextView tvSpeed = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_speed);
TextView tvDimensions = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_dimensions);
TextView tvDraft = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_draft);
TextView tvDestination = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_destination);
TextView tvEta = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_eta);
TextView tvNavStatus = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_nav_status);
TextView tvClass = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_class);
TextView tvSignal = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_signal);
TextView tvDistance = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_distance);
TextView tvBearing = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_bearing);
TextView tvLastUpdate = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_last_update);
TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago);
if (tvTitle != null) {
String name = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty() ? vessel.getVesselName() : "AIS СУДНО";
String flag = getFlagEmojiForMMSI(vessel.getMmsi());
tvTitle.setText((flag != null ? flag + " " : "") + "🚢 " + name);
}
if (tvMmsi != null) tvMmsi.setText("🆔 MMSI: " + (vessel.getMmsi() != null ? vessel.getMmsi() : "--"));
if (tvCallsign != null) tvCallsign.setText("📻 Позывной: " + (vessel.getCallSign() != null ? vessel.getCallSign() : "--"));
if (tvImo != null) tvImo.setText("🏷️ IMO: " + (vessel.getImo() > 0 ? String.valueOf(vessel.getImo()) : "--"));
if (tvType != null) tvType.setText("🚢 Тип: " + (vessel.getVesselType() != null ? vessel.getVesselType() : "--"));
if (tvPosition != null) {
if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) {
tvPosition.setText(String.format("📍 Координаты: %.6f, %.6f", vessel.getLatitude(), vessel.getLongitude()));
} else {
tvPosition.setText("📍 Координаты: --");
}
}
if (tvCourse != null) tvCourse.setText(vessel.getCourse() > 0 ? String.format("🧭 COG: %.1f°", vessel.getCourse()) : "🧭 COG: --°");
if (tvRot != null) tvRot.setText(vessel.getRateOfTurn() != 0 ? String.format("🔄 ROT: %.1f°/мин", vessel.getRateOfTurn()) : "🔄 ROT: --°/мин");
if (tvHeading != null) tvHeading.setText(vessel.getHeading() > 0 ? String.format("🧭 HDG: %.1f°", vessel.getHeading()) : "🧭 HDG: --°");
if (tvSpeed != null) tvSpeed.setText(vessel.getSpeed() > 0 ? String.format("⚡ Скорость: %.1f узлов", vessel.getSpeed()) : "⚡ Скорость: -- узлов");
if (tvDimensions != null) tvDimensions.setText((vessel.getLength() > 0 && vessel.getWidth() > 0) ? String.format("📏 Размеры: %.1f x %.1f м", vessel.getLength(), vessel.getWidth()) : "📏 Размеры: --");
if (tvDraft != null) tvDraft.setText(vessel.getDraft() > 0 ? String.format("🌊 Осадка: %.1f м", vessel.getDraft()) : "🌊 Осадка: -- м");
if (tvDestination != null) tvDestination.setText("🎯 Назначение: " + (vessel.getDestination() != null ? vessel.getDestination() : "--"));
if (tvEta != null) tvEta.setText(vessel.getEta() != null ? String.format("⏰ ETA: %s", vessel.getEta().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))) : "⏰ ETA: --");
if (tvNavStatus != null) tvNavStatus.setText("🚦 Статус: " + (vessel.getNavigationalStatus() != null ? vessel.getNavigationalStatus() : "--"));
if (tvClass != null) tvClass.setText("📋 Класс: " + (vessel.getVesselClass() != null ? vessel.getVesselClass() : "--"));
if (tvSignal != null) {
if (vessel.getSignalStrength() > 0) {
tvSignal.setText(String.format("📶 Сигнал: %d", vessel.getSignalStrength()));
} else {
tvSignal.setText(vessel.isPositionAccuracy() ? "📶 Точность: высокая" : "📶 Точность: низкая");
}
}
if (tvLastUpdate != null) tvLastUpdate.setText(vessel.getLastUpdate() != null ? String.format("🕐 Обновлено: %s", vessel.getLastUpdate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"))) : "🕐 Обновлено: --");
if (tvDistance != null || tvBearing != null) {
Vessel ourVessel = appCoordinator.getOwnVessel();
if (ourVessel != null && ourVessel.getLatitude() != 0 && ourVessel.getLongitude() != 0 && vessel.getLatitude() != 0 && vessel.getLongitude() != 0) {
double distance = com.grigowashere.aismap.utils.NavigationUtils.calculateDistance(ourVessel.getLatitude(), ourVessel.getLongitude(), vessel.getLatitude(), vessel.getLongitude());
if (tvDistance != null) tvDistance.setText("📏 Расстояние: " + com.grigowashere.aismap.utils.NavigationUtils.formatDistance(distance));
double bearing = com.grigowashere.aismap.utils.NavigationUtils.calculateBearing(ourVessel.getLatitude(), ourVessel.getLongitude(), vessel.getLatitude(), vessel.getLongitude());
double relativeBearing = com.grigowashere.aismap.utils.NavigationUtils.calculateRelativeBearing(ourVessel.getCourse(), bearing);
if (tvBearing != null) tvBearing.setText("🧭 Пеленг: " + com.grigowashere.aismap.utils.NavigationUtils.formatRelativeBearing(relativeBearing));
} else {
if (tvDistance != null) tvDistance.setText("📏 Расстояние: --");
if (tvBearing != null) tvBearing.setText("🧭 Пеленг: --");
}
}
if (tvTimeAgo != null) {
if (vessel.getLastUpdate() != null) {
long secondsAgo = java.time.Duration.between(vessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds();
tvTimeAgo.setText("⏱️ Время назад: " + formatTimeAgo(secondsAgo));
} else {
tvTimeAgo.setText("⏱️ Время назад: --");
}
}
}
public void startTimeUpdate() {
if (timeUpdateHandler != null && timeUpdateRunnable != null) {
timeUpdateHandler.postDelayed(timeUpdateRunnable, 1000);
}
}
public void stopTimeUpdate() {
if (timeUpdateHandler != null && timeUpdateRunnable != null) {
timeUpdateHandler.removeCallbacks(timeUpdateRunnable);
}
currentAISVessel = null;
}
public void startBottomSheetAutoUpdate() {
if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) {
bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable);
bottomSheetUpdateHandler.postDelayed(bottomSheetUpdateRunnable, BOTTOM_SHEET_UPDATE_INTERVAL);
Log.i(TAG, "Автоматическое обновление BottomSheet запущено");
}
}
public void stopBottomSheetAutoUpdate() {
if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) {
bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable);
Log.i(TAG, "Автоматическое обновление BottomSheet остановлено");
}
}
public void updateAISTimeAgo() {
if (aisBottomSheetView == null || currentAISVessel == null) return;
TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago);
if (tvTimeAgo != null && currentAISVessel.getLastUpdate() != null) {
long secondsAgo = java.time.Duration.between(currentAISVessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds();
tvTimeAgo.setText("⏱️ Время назад: " + formatTimeAgo(secondsAgo));
}
}
public void cleanup() {
stopTimeUpdate();
stopBottomSheetAutoUpdate();
try { if (ownVesselBottomSheet != null) ownVesselBottomSheet.dismiss(); } catch (Exception ignore) {}
try { if (aisVesselBottomSheet != null) aisVesselBottomSheet.dismiss(); } catch (Exception ignore) {}
}
private String formatTimeAgo(long seconds) {
if (seconds < 60) return seconds + " сек";
if (seconds < 3600) return (seconds / 60) + " мин";
if (seconds < 86400) return (seconds / 3600) + " ч";
return (seconds / 86400) + " дн";
}
private String getFlagEmojiForMMSI(String mmsi) {
try {
if (mmsi == null || mmsi.length() < 3) return null;
String mid = mmsi.substring(0, 3);
String iso2 = com.grigowashere.aismap.utils.MIDToCountry.MID_TO_COUNTRY.get(mid);
if (iso2 == null || iso2.length() != 2) return null;
char a = Character.toUpperCase(iso2.charAt(0));
char b = Character.toUpperCase(iso2.charAt(1));
int base = 0x1F1E6;
int cp1 = base + (a - 'A');
int cp2 = base + (b - 'A');
return new String(Character.toChars(cp1)) + new String(Character.toChars(cp2));
} catch (Exception ignored) {
return null;
}
}
}
@@ -0,0 +1,101 @@
package com.grigowashere.aismap.ui;
import android.view.Menu;
import android.view.MenuItem;
import android.util.Log;
import com.grigowashere.aismap.R;
import com.grigowashere.aismap.controllers.AppCoordinator;
import com.grigowashere.aismap.utils.SettingsManager;
/**
* Отвечает за работу с меню в MainActivity: создание и обработка пунктов
*/
public class MenuBinder {
private static final String TAG = "MenuBinder";
private final AppCoordinator appCoordinator;
private final SettingsManager settingsManager;
private final MenuActions actions;
public MenuBinder(AppCoordinator appCoordinator, SettingsManager settingsManager, MenuActions actions) {
this.appCoordinator = appCoordinator;
this.settingsManager = settingsManager;
this.actions = actions;
}
public boolean onCreateOptionsMenu(Menu menu) {
// Пока без переноса инфлейта; оставим точку расширения
Log.d(TAG, "onCreateOptionsMenu: ready");
return true;
}
public boolean onPrepareOptionsMenu(Menu menu) {
try {
MenuItem gpsItem = menu.findItem(R.id.menu_gps);
MenuItem udpItem = menu.findItem(R.id.menu_udp);
if (gpsItem != null) {
gpsItem.setTitle(appCoordinator.isAndroidNMEAEnabled() ? "GPS ✓" : "GPS");
}
if (udpItem != null) {
udpItem.setTitle(appCoordinator.isUDPEnabled() ? "UDP ✓" : "UDP");
}
MenuItem pathItem = menu.findItem(R.id.menu_path_tracking);
if (pathItem != null) {
boolean pathEnabled = settingsManager.isPathTrackingEnabled();
pathItem.setTitle(pathEnabled ? "Пути ✓" : "Пути");
}
MenuItem screenItem = menu.findItem(R.id.menu_keep_screen_on);
if (screenItem != null) {
boolean screenEnabled = settingsManager.isKeepScreenOnEnabled();
screenItem.setTitle(screenEnabled ? "Экран ✓" : "Экран");
}
return true;
} catch (Exception e) {
Log.w(TAG, "onPrepareOptionsMenu: " + e.getMessage());
return false;
}
}
public boolean onOptionsItemSelected(MenuItem item) {
if (item == null) return false;
int id = item.getItemId();
try {
if (id == R.id.menu_gps) {
actions.toggleGPS();
return true;
} else if (id == R.id.menu_udp) {
actions.toggleUDP();
return true;
} else if (id == R.id.menu_clear_ais) {
actions.clearAIS();
return true;
} else if (id == R.id.menu_path_tracking) {
actions.togglePathTracking();
return true;
} else if (id == R.id.menu_service_test) {
actions.testForegroundService();
return true;
} else if (id == R.id.menu_keep_screen_on) {
actions.toggleKeepScreenOn();
return true;
}
} catch (Exception e) {
Log.w(TAG, "onOptionsItemSelected error: " + e.getMessage());
}
return false;
}
/**
* Действия меню, которые реализует MainActivity
*/
public interface MenuActions {
void toggleGPS();
void toggleUDP();
void clearAIS();
void togglePathTracking();
void testForegroundService();
void toggleKeepScreenOn();
}
}
@@ -0,0 +1,42 @@
package com.grigowashere.aismap.ui;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.util.Log;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
/**
* Инкапсулирует проверку и запрос runtime-разрешений
*/
public class PermissionsBinder {
private static final String TAG = "PermissionsBinder";
private final Activity activity;
public PermissionsBinder(Activity activity) {
this.activity = activity;
}
public boolean ensurePermission(String permission, int requestCode) {
if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) {
return true;
}
Log.d(TAG, "Requesting permission: " + permission);
ActivityCompat.requestPermissions(activity, new String[]{permission}, requestCode);
return false;
}
public boolean handleOnRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults, int expectedCode, Runnable onGranted, Runnable onDenied) {
if (requestCode != expectedCode) return false;
boolean granted = grantResults != null && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
if (granted) {
if (onGranted != null) onGranted.run();
} else {
if (onDenied != null) onDenied.run();
}
return true;
}
}
@@ -5,6 +5,7 @@ import android.os.Looper;
import android.util.Log;
import com.grigowashere.aismap.maps.MapInterface;
import com.grigowashere.aismap.maps.MapInterfaceChangeListener;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
@@ -17,8 +18,9 @@ import java.util.HashMap;
* Координатор UI отрисовки
* Единая точка всех операций с картой и UI
* Обеспечивает throttling и батчинг операций
* Подписывается на изменения MapInterface для автоматического обновления
*/
public class UIRenderingCoordinator implements UIDataChangeNotifier {
public class UIRenderingCoordinator implements UIDataChangeNotifier, MapInterfaceChangeListener {
private static final String TAG = "UIRenderingCoordinator";
// Throttling интервалы
@@ -178,8 +180,10 @@ public class UIRenderingCoordinator implements UIDataChangeNotifier {
if (mapInterface == null) return;
try {
// TODO: Реализовать батчинговое обновление путей
Log.d(TAG, "Path updates выполнены (заглушка)");
// Обновляем пути на карте
// MapInterface должен обновить все пути AIS судов
mapInterface.updateAllVesselPaths();
Log.d(TAG, "Path updates выполнены");
} catch (Exception e) {
Log.e(TAG, "Ошибка path updates: " + e.getMessage(), e);
}
@@ -275,4 +279,91 @@ public class UIRenderingCoordinator implements UIDataChangeNotifier {
// Компас не относится к карте, передаем в MainActivity через callback
Log.d(TAG, "Compass update: " + azimuth + "° - требует специальной обработки в MainActivity");
}
/**
* Реализация MapInterfaceChangeListener
* Вызывается при смене MapInterface в MapController
*/
@Override
public void onMapInterfaceChanged(MapInterface oldMapInterface, MapInterface newMapInterface) {
Log.i(TAG, "🔄 MapInterface изменен в UIRenderingCoordinator");
Log.i(TAG, " Старый: " + (oldMapInterface != null ? oldMapInterface.getClass().getSimpleName() : "null"));
Log.i(TAG, " Новый: " + (newMapInterface != null ? newMapInterface.getClass().getSimpleName() : "null"));
// Обновляем ссылку на MapInterface
this.mapInterface = newMapInterface;
if (newMapInterface != null) {
Log.i(TAG, "✅ UIRenderingCoordinator обновлен с новым MapInterface");
// Переносим pending операции на новую карту
transferPendingOperationsToNewMap();
} else {
Log.w(TAG, "⚠️ Новый MapInterface равен null - очищаем pending операции");
clearPendingOperations();
}
}
/**
* Переносит pending операции на новую карту
*/
private void transferPendingOperationsToNewMap() {
if (mapInterface == null) {
Log.w(TAG, "⚠️ MapInterface равен null, нельзя перенести операции");
return;
}
Log.i(TAG, "🔄 Перенос pending операций на новую карту");
// Выполняем pending операции немедленно на новой карте
uiHandler.post(() -> {
try {
// Выполняем pending vessel update
if (pendingVesselUpdate != null) {
Log.d(TAG, "📍 Выполняем pending vessel update");
executeVesselUpdate();
}
// Выполняем pending AIS updates
if (!pendingAISUpdates.isEmpty()) {
Log.d(TAG, "🚢 Выполняем " + pendingAISUpdates.size() + " pending AIS updates");
executeAISUpdates();
}
// Выполняем pending AIS removals
if (!pendingAISRemovals.isEmpty()) {
Log.d(TAG, "🗑️ Выполняем " + pendingAISRemovals.size() + " pending AIS removals");
executeAISUpdates(); // Этот метод обрабатывает и удаления тоже
}
Log.i(TAG, "✅ Pending операции перенесены на новую карту");
} catch (Exception e) {
Log.e(TAG, "❌ Ошибка при переносе pending операций: " + e.getMessage(), e);
}
});
}
/**
* Очищает все pending операции
*/
private void clearPendingOperations() {
Log.i(TAG, "🧹 Очистка всех pending операций");
pendingVesselUpdate = null;
pendingAISUpdates.clear();
pendingAISRemovals.clear();
// Сбрасываем флаги
vesselUpdatePending = false;
aisUpdatePending = false;
pathUpdatePending = false;
// Отменяем все запланированные операции
uiHandler.removeCallbacks(vesselUpdateRunnable);
uiHandler.removeCallbacks(aisUpdateRunnable);
uiHandler.removeCallbacks(pathUpdateRunnable);
Log.i(TAG, "✅ Pending операции очищены");
}
}
@@ -32,6 +32,8 @@ public class SettingsManager {
private static final String KEY_SOUND_ENABLED = "sound_enabled";
private static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled";
private static final String KEY_CURSOR_ENABLED = "cursor_enabled";
private static final String KEY_NOTIFICATIONS_ENABLED = "notifications_enabled";
private static final String KEY_ANDROID_GPS_ENABLED = "android_gps_enabled";
// Значения по умолчанию
private static final int DEFAULT_UDP_PORT = 10110;
@@ -52,6 +54,8 @@ public class SettingsManager {
private static final boolean DEFAULT_SOUND_ENABLED = true;
private static final boolean DEFAULT_KEEP_SCREEN_ON_ENABLED = true;
private static final boolean DEFAULT_CURSOR_ENABLED = false;
private static final boolean DEFAULT_NOTIFICATIONS_ENABLED = true;
private static final boolean DEFAULT_ANDROID_GPS_ENABLED = true;
// Режимы работы с данными
public static final String DATA_MODE_HYBRID = "hybrid";
@@ -179,6 +183,21 @@ public class SettingsManager {
return DATA_MODE_ANDROID_ONLY.equals(getDataMode());
}
/**
* Проверяет, включен ли Android GPS (Location API)
*/
public boolean isAndroidGPSEnabled() {
return prefs.getBoolean(KEY_ANDROID_GPS_ENABLED, DEFAULT_ANDROID_GPS_ENABLED);
}
/**
* Включает/выключает Android GPS (Location API)
*/
public void setAndroidGPSEnabled(boolean enabled) {
prefs.edit().putBoolean(KEY_ANDROID_GPS_ENABLED, enabled).apply();
Log.i(TAG, "Android GPS: " + (enabled ? "включен" : "выключен"));
}
/**
* Сбрасывает все настройки к значениям по умолчанию
*/
@@ -435,4 +454,19 @@ public class SettingsManager {
Log.i(TAG, "Курсор на карте: " + (enabled ? "включен" : "выключен"));
}
/**
* Проверяет, включены ли уведомления
*/
public boolean areNotificationsEnabled() {
return prefs.getBoolean(KEY_NOTIFICATIONS_ENABLED, DEFAULT_NOTIFICATIONS_ENABLED);
}
/**
* Включает/выключает уведомления
*/
public void setNotificationsEnabled(boolean enabled) {
prefs.edit().putBoolean(KEY_NOTIFICATIONS_ENABLED, enabled).apply();
Log.i(TAG, "Уведомления: " + (enabled ? "включены" : "выключены"));
}
}