Подготовка к крупным изменениям: карта, AIS и UI

- Яндекс/MapForge: правки в менеджерах и обёртках маркеров (улучшена отрисовка/логика)
- NMEAParser: корректировки парсинга и стабильности
- Модель AISVessel: уточнение полей/логики
- Настройки: правки в SettingsActivity и SettingsManager, актуализация AppController
- UI: обновлены activity_main, activity_settings, bottom_sheet_ais_vessel; меню main_menu
- Ресурсы: добавлен drawable/targetclassa.xml, обновлён drawable/target.xml
- Конфигурация: правки AndroidManifest и app/build.gradle
- Прочее: изменения в .idea (не влияют на сборку)
This commit is contained in:
2025-09-23 11:53:23 +03:00
parent a2f1775f9f
commit 41432665ea
37 changed files with 6561 additions and 161 deletions
@@ -5,6 +5,9 @@ import android.util.Log;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.maps.MapInterface;
import com.grigowashere.aismap.data.Repository;
import com.grigowashere.aismap.data.mapper.AISVesselMapper;
import com.grigowashere.aismap.services.NotificationService;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
@@ -34,6 +37,8 @@ public class AppController implements
private Vessel ownVessel;
private List<AISVessel> aisVessels;
private ExecutorService executor;
private com.grigowashere.aismap.data.Repository repository;
private NotificationService notificationService;
private boolean isUDPEnabled;
private boolean isAndroidNMEAEnabled;
@@ -42,6 +47,15 @@ public class AppController implements
private int udpPort;
private String dataMode;
// Время последнего получения сообщений ($ GPS) и (! AIS) в elapsedRealtime
private long lastGPSMessageRealtimeMs;
private long lastAISMessageRealtimeMs;
// Периодическая очистка БД от устаревших AIS целей
private android.os.Handler dbCleanupHandler;
private Runnable dbCleanupRunnable;
private static final long DB_CLEANUP_INTERVAL = 60000; // 1 минута
// Callback для обновления UI
private UIUpdateCallback uiUpdateCallback;
@@ -64,6 +78,12 @@ public class AppController implements
this.ownVessel = new Vessel();
this.aisVessels = new ArrayList<>();
this.executor = Executors.newCachedThreadPool();
this.repository = new com.grigowashere.aismap.data.Repository(context);
this.notificationService = new NotificationService(context);
// Инициализируем Handler для периодической очистки БД
this.dbCleanupHandler = new android.os.Handler(android.os.Looper.getMainLooper());
this.dbCleanupRunnable = this::performDatabaseCleanup;
initializeControllers();
}
@@ -92,6 +112,29 @@ public class AppController implements
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
androidNmeaListener = new AndroidNMEAListener(context);
androidNmeaListener.setCallback(this);
// Восстанавливаем данные из БД при старте
try {
com.grigowashere.aismap.data.entity.VesselEntity latest = repository.getLatestOwnVesselSync();
if (latest != null) {
ownVessel.setLatitude(latest.latitude);
ownVessel.setLongitude(latest.longitude);
ownVessel.setAccuracy(latest.accuracy);
ownVessel.setFixTime(latest.fixTime);
}
java.util.List<com.grigowashere.aismap.data.entity.AISVesselEntity> list = repository.getAllAISSync();
if (list != null) {
for (com.grigowashere.aismap.data.entity.AISVesselEntity entity : list) {
// Используем маппер для полного восстановления всех полей
AISVessel vessel = AISVesselMapper.toModel(entity);
aisVessels.add(vessel);
Log.d(TAG, "AIS судно восстановлено из БД с полными данными: " + vessel.getMmsi());
}
Log.i(TAG, "Восстановлено " + list.size() + " AIS судов из БД с полными данными");
}
} catch (Exception e) {
Log.e(TAG, "Ошибка восстановления данных из БД: " + e.getMessage(), e);
}
}
/**
@@ -104,6 +147,24 @@ public class AppController implements
Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface");
mapInterface.setMarkerClickListener(this);
Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры");
// Восстановление отрисовки сохранённых данных на карте
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
try {
// Позиция нашего судна
if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) {
mapInterface.updateOwnVesselPosition(ownVessel);
}
// AIS маркеры
if (aisVessels != null && !aisVessels.isEmpty()) {
for (AISVessel v : aisVessels) {
mapInterface.addAISVesselMarker(v);
}
}
} catch (Exception e) {
Log.e(TAG, "Ошибка восстановления отрисовки на карте: " + e.getMessage(), e);
}
});
}
}
@@ -135,7 +196,8 @@ public class AppController implements
});
}
// Запускаем периодическую очистку БД от устаревших AIS целей
startDatabaseCleanup();
}
@@ -144,6 +206,9 @@ public class AppController implements
* Останавливает все слушатели
*/
public void stopAllListeners() {
// Останавливаем периодическую очистку БД
stopDatabaseCleanup();
executor.execute(() -> {
udpListener.stop();
androidNmeaListener.stopListening();
@@ -279,15 +344,28 @@ public class AppController implements
ownVessel.setFixTime(vessel.getFixTime());
ownVessel.setFixQuality(vessel.getFixQuality());
// Сохраняем позицию в локальную БД
try {
com.grigowashere.aismap.data.entity.VesselEntity ve = new com.grigowashere.aismap.data.entity.VesselEntity();
ve.latitude = ownVessel.getLatitude();
ve.longitude = ownVessel.getLongitude();
ve.accuracy = ownVessel.getAccuracy();
ve.fixTime = ownVessel.getFixTime();
repository.upsertOwnVessel(ve);
} catch (Exception e) {
Log.e(TAG, "Ошибка сохранения позиции в БД: " + e.getMessage(), e);
}
// Обновляем UI через callback
if (uiUpdateCallback != null) {
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
}
// Обновляем карту в главном потоке
// Обновляем карту в главном потоке с throttling
if (mapInterface != null) {
Log.i(TAG, "Обновляем позицию на карте...");
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
// Используем postDelayed для предотвращения частых обновлений
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
try {
Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition...");
mapInterface.updateOwnVesselPosition(ownVessel);
@@ -295,7 +373,7 @@ public class AppController implements
} catch (Exception e) {
Log.e(TAG, "Ошибка обновления позиции на карте: " + e.getMessage(), e);
}
});
}, 100); // Задержка 100мс для throttling
}
}
@@ -381,6 +459,17 @@ public class AppController implements
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
if (existingVessel != null) {
// Если пришло новое safety-сообщение (тип 14), уведомим пользователя
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
String prev = existingVessel.getLastSafetyMessage();
String curr = vessel.getLastSafetyMessage();
if (prev == null || !prev.equals(curr)) {
if (notificationService != null && notificationService.areNotificationsEnabled()) {
notificationService.notifySafetyMessage(vessel.getMmsi(), curr);
}
}
existingVessel.setLastSafetyMessage(curr);
}
// Обновляем существующее судно
existingVessel.updatePosition(
vessel.getLatitude(),
@@ -388,6 +477,14 @@ public class AppController implements
vessel.getCourse(),
vessel.getSpeed()
);
try {
// Используем маппер для полной конвертации всех полей
com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(existingVessel);
repository.upsertAIS(entity);
Log.d(TAG, "AIS судно сохранено в БД с полными данными: " + existingVessel.getMmsi());
} catch (Exception e) {
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
}
if (mapInterface != null) {
// Используем Handler для выполнения в главном потоке
@@ -402,6 +499,28 @@ public class AppController implements
} else {
// Добавляем новое судно
aisVessels.add(vessel);
// Если это новое судно сразу пришло с safety-сообщением — уведомим
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
if (notificationService != null && notificationService.areNotificationsEnabled()) {
notificationService.notifySafetyMessage(vessel.getMmsi(), vessel.getLastSafetyMessage());
}
}
// Воспроизводим уведомление о новой цели
if (notificationService != null && notificationService.areNotificationsEnabled()) {
notificationService.notifyNewAISTarget();
Log.i(TAG, "🔔 Уведомление о новой AIS цели: " + vessel.getMmsi());
}
try {
// Используем маппер для полной конвертации всех полей
com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(vessel);
repository.upsertAIS(entity);
Log.d(TAG, "Новое AIS судно сохранено в БД с полными данными: " + vessel.getMmsi());
} catch (Exception e) {
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
}
if (mapInterface != null) {
// Используем Handler для выполнения в главном потоке
@@ -465,6 +584,8 @@ public class AppController implements
// Парсим полученные данные как NMEA
nmeaParser.parseNMEA(data);
// Обновляем метки времени по префиксу
updateLastMessageAgesFromRaw(data);
}
@Override
@@ -485,6 +606,18 @@ public class AppController implements
// Парсим полученные данные как NMEA
nmeaParser.parseNMEA(message);
if (message != null) {
String trimmed = message.trim();
if (!trimmed.isEmpty()) {
char c = trimmed.charAt(0);
long now = android.os.SystemClock.elapsedRealtime();
if (c == '$') {
lastGPSMessageRealtimeMs = now;
} else if (c == '!') {
lastAISMessageRealtimeMs = now;
}
}
}
}
// Реализация MarkerClickListener
@@ -577,11 +710,56 @@ public class AppController implements
}
}
/**
* Запускает периодическую очистку БД от устаревших AIS целей
*/
public void startDatabaseCleanup() {
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
Log.i(TAG, "Запущена периодическая очистка БД от устаревших AIS целей");
}
}
/**
* Останавливает периодическую очистку БД
*/
public void stopDatabaseCleanup() {
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
dbCleanupHandler.removeCallbacks(dbCleanupRunnable);
Log.i(TAG, "Остановлена периодическая очистка БД от устаревших AIS целей");
}
}
/**
* Выполняет очистку БД от устаревших AIS целей
*/
private void performDatabaseCleanup() {
try {
com.grigowashere.aismap.utils.SettingsManager settingsManager =
new com.grigowashere.aismap.utils.SettingsManager(context);
int staleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes();
long thresholdEpochMs = System.currentTimeMillis() - (staleRemoveMinutes * 60 * 1000L);
repository.deleteStaleAIS(thresholdEpochMs);
Log.i(TAG, "Выполнена очистка БД от AIS целей старше " + staleRemoveMinutes + " минут");
// Планируем следующую очистку
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
}
} catch (Exception e) {
Log.e(TAG, "Ошибка при очистке БД от устаревших AIS целей: " + e.getMessage(), e);
}
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopAllListeners();
stopDatabaseCleanup();
if (udpListener != null) {
udpListener.cleanup();
@@ -595,11 +773,51 @@ public class AppController implements
gpsLocationListener.cleanup();
}
if (notificationService != null) {
notificationService.cleanup();
}
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
// ===== Метки времени последних сообщений ($ и !) =====
private void updateLastMessageAgesFromRaw(String raw) {
if (raw == null) return;
long now = android.os.SystemClock.elapsedRealtime();
String[] lines = raw.split("\r?\n");
for (String line : lines) {
if (line == null) continue;
String t = line.trim();
if (t.isEmpty()) continue;
char c = t.charAt(0);
if (c == '$') {
lastGPSMessageRealtimeMs = now;
break;
} else if (c == '!') {
lastAISMessageRealtimeMs = now;
break;
}
}
}
/** Возвращает секунды с последнего GPS ($) сообщения; -1 если не было */
public int getSecondsSinceLastGPSMessage() {
if (lastGPSMessageRealtimeMs <= 0) return -1;
long diff = android.os.SystemClock.elapsedRealtime() - lastGPSMessageRealtimeMs;
if (diff < 0) return 0;
return (int)(diff / 1000L);
}
/** Возвращает секунды с последнего AIS (!) сообщения; -1 если не было */
public int getSecondsSinceLastAISMessage() {
if (lastAISMessageRealtimeMs <= 0) return -1;
long diff = android.os.SystemClock.elapsedRealtime() - lastAISMessageRealtimeMs;
if (diff < 0) return 0;
return (int)(diff / 1000L);
}
// Методы для управления настройками
/**