feat: новая архитектура UI и расширенная визуализация AIS

Архитектурные улучшения:
- Внедрен UIRenderingCoordinator с централизованным throttling
- Решены проблемы зависания UI через батчинг операций карты
- Добавлен VesselPathController для отслеживания маршрутов
- Реализован MapLibreMapImpl как альтернатива Яндекс.Картам

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

Производительность:
- Throttling UI обновлений (vessel: 500ms, AIS: 1s, paths: 2s)
- Устранение утечек Handler объектов
- Оптимизация GeoJSON операций в MapLibre
This commit is contained in:
2025-10-02 09:15:33 +03:00
parent 41432665ea
commit b5aee265bc
85 changed files with 7132 additions and 449 deletions
@@ -0,0 +1,55 @@
package com.grigowashere.aismap.ui;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
/**
* Интерфейс для уведомлений UI о изменениях данных
* Контроллеры используют этот интерфейс для информирования UI о изменениях
* без знания деталей UI реализации
*/
public interface UIDataChangeNotifier {
/**
* Уведомление об изменении позиции собственного судна
* @param vessel обновленные данные судна
*/
void onVesselPositionChanged(Vessel vessel);
/**
* Уведомление об изменении качества GPS данных
* @param vessel данные судна с обновленными GPS метаданными
*/
void onGPSQualityChanged(Vessel vessel);
/**
* Уведомление о новой AIS судне или обновлении существующего
* @param vessel данные AIS судна
*/
void onAISVesselChanged(AISVessel vessel);
/**
* Уведомление об удалении AIS судна
* @param mmsi идентификатор удаляемого судна
*/
void onAISVesselRemoved(String mmsi);
/**
* Уведомление об изменении пути судна
* @param mmsi идентификатор судна (null для собственного судна)
*/
void onVesselPathChanged(String mmsi);
/**
* Уведомление о центрировании карты
* @param latitude широта
* @param longitude долгота
*/
void onRequestCenterMap(double latitude, double longitude);
/**
* Уведомление об обновлении компаса
* @param azimuth значение азимута
*/
void onCompassUpdate(float azimuth);
}
@@ -0,0 +1,278 @@
package com.grigowashere.aismap.ui;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.grigowashere.aismap.maps.MapInterface;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.HashMap;
/**
* Координатор UI отрисовки
* Единая точка всех операций с картой и UI
* Обеспечивает throttling и батчинг операций
*/
public class UIRenderingCoordinator implements UIDataChangeNotifier {
private static final String TAG = "UIRenderingCoordinator";
// Throttling интервалы
public static final long VESSEL_UPDATE_THROTTLE = 500; // 500мс для позиции судна
public static final long AIS_UPDATE_THROTTLE = 1000; // 1сек для AIS данных
public static final long PATH_UPDATE_THROTTLE = 2000; // 2сек для путей
private MapInterface mapInterface;
private Handler uiHandler;
// Pending операции для батчинга
private Vessel pendingVesselUpdate;
private final Map<String, AISVessel> pendingAISUpdates = new HashMap<>();
private final Set<String> pendingAISRemovals = new HashSet<>();
// Throttling Runnable's
private Runnable vesselUpdateRunnable;
private Runnable aisUpdateRunnable;
private Runnable pathUpdateRunnable;
// Флаги для предотвращения множественных запланированных операций
private boolean vesselUpdatePending = false;
private boolean aisUpdatePending = false;
private boolean pathUpdatePending = false;
public UIRenderingCoordinator(MapInterface mapInterface) {
this.mapInterface = mapInterface;
this.uiHandler = new Handler(Looper.getMainLooper());
setupThrottling();
Log.i(TAG, "UIRenderingCoordinator инициализирован");
}
/**
* Настройка throttling механизмов
*/
private void setupThrottling() {
vesselUpdateRunnable = () -> {
vesselUpdatePending = false;
executeVesselUpdate();
};
aisUpdateRunnable = () -> {
aisUpdatePending = false;
executeAISUpdates();
};
pathUpdateRunnable = () -> {
pathUpdatePending = false;
executePathUpdates();
};
}
/**
* Запрос обновления позиции собственного судна
*/
public void requestVesselUpdate(Vessel vessel) {
if (vessel == null) return;
pendingVesselUpdate = vessel;
if (!vesselUpdatePending) {
vesselUpdatePending = true;
uiHandler.removeCallbacks(vesselUpdateRunnable);
uiHandler.postDelayed(vesselUpdateRunnable, VESSEL_UPDATE_THROTTLE);
Log.d(TAG, "Vessel update запланирован на " + VESSEL_UPDATE_THROTTLE + "мс");
}
}
/**
* Запрос обновления AIS судна
*/
public void requestAISUpdate(AISVessel vessel) {
if (vessel == null || vessel.getMmsi() == null) return;
pendingAISUpdates.put(vessel.getMmsi(), vessel);
if (!aisUpdatePending) {
aisUpdatePending = true;
uiHandler.removeCallbacks(aisUpdateRunnable);
uiHandler.postDelayed(aisUpdateRunnable, AIS_UPDATE_THROTTLE);
Log.d(TAG, "AIS update запланирован на " + AIS_UPDATE_THROTTLE + "мс");
}
}
/**
* Запрос удаления AIS судна
*/
public void requestAISRemoval(String mmsi) {
if (mmsi == null) return;
pendingAISRemovals.add(mmsi);
pendingAISUpdates.remove(mmsi); // Убираем из обновлений
if (!aisUpdatePending) {
aisUpdatePending = true;
uiHandler.removeCallbacks(aisUpdateRunnable);
uiHandler.postDelayed(aisUpdateRunnable, AIS_UPDATE_THROTTLE);
Log.d(TAG, "AIS removal запланирован на " + AIS_UPDATE_THROTTLE + "мс");
}
}
/**
* Выполнение обновления позиции судна
*/
private void executeVesselUpdate() {
if (mapInterface == null || pendingVesselUpdate == null) return;
try {
Log.d(TAG, "Выполняем vessel update: " + pendingVesselUpdate.getLatitude() + "," + pendingVesselUpdate.getLongitude());
mapInterface.updateOwnVesselPosition(pendingVesselUpdate);
Log.d(TAG, "Vessel update выполнен успешно");
} catch (Exception e) {
Log.e(TAG, "Ошибка vessel update: " + e.getMessage(), e);
}
pendingVesselUpdate = null;
}
/**
* Выполнение обновлений AIS судов
*/
private void executeAISUpdates() {
if (mapInterface == null) return;
try {
// Удаляем старые суда
for (String mmsi : pendingAISRemovals) {
Log.d(TAG, "Удаляем AIS судно: " + mmsi);
mapInterface.removeAISVesselMarker(mmsi);
}
// Обновляем или добавляем суда (различать не будем - MapInterface сам решит)
for (AISVessel vessel : pendingAISUpdates.values()) {
Log.d(TAG, "Обновляем/добавляем AIS судно: " + vessel.getMmsi());
mapInterface.updateAISVesselPosition(vessel);
}
Log.d(TAG, "AIS updates выполнены: удалено=" + pendingAISRemovals.size() +
", обновлено=" + pendingAISUpdates.size());
} catch (Exception e) {
Log.e(TAG, "Ошибка AIS updates: " + e.getMessage(), e);
}
// Очищаем pending операции
pendingAISUpdates.clear();
pendingAISRemovals.clear();
}
/**
* Выполнение обновлений путей (заглушка для будущего)
*/
private void executePathUpdates() {
if (mapInterface == null) return;
try {
// TODO: Реализовать батчинговое обновление путей
Log.d(TAG, "Path updates выполнены (заглушка)");
} catch (Exception e) {
Log.e(TAG, "Ошибка path updates: " + e.getMessage(), e);
}
}
/**
* Принудительное выполнение всех pending операций
*/
public void flushPendingOperations() {
Log.i(TAG, "Принудительное выполнение всех pending операций");
if (uiHandler != null) {
uiHandler.removeCallbacks(vesselUpdateRunnable);
uiHandler.removeCallbacks(aisUpdateRunnable);
uiHandler.removeCallbacks(pathUpdateRunnable);
}
vesselUpdatePending = false;
aisUpdatePending = false;
pathUpdatePending = false;
executeVesselUpdate();
executeAISUpdates();
executePathUpdates();
Log.i(TAG, "Все pending операции выполнены");
}
/**
* Очистка ресурсов
*/
public void cleanup() {
Log.i(TAG, "Очистка UIRenderingCoordinator");
if (uiHandler != null) {
uiHandler.removeCallbacksAndMessages(null);
}
flushPendingOperations();
mapInterface = null;
Log.i(TAG, "UIRenderingCoordinator очищен");
}
// ========== Реализация UIDataChangeNotifier ==========
@Override
public void onVesselPositionChanged(Vessel vessel) {
requestVesselUpdate(vessel);
}
@Override
public void onGPSQualityChanged(Vessel vessel) {
// GPS качество влияет на отображение точности, но не требует urgent update
requestVesselUpdate(vessel);
}
@Override
public void onAISVesselChanged(AISVessel vessel) {
requestAISUpdate(vessel);
}
@Override
public void onAISVesselRemoved(String mmsi) {
requestAISRemoval(mmsi);
}
@Override
public void onVesselPathChanged(String mmsi) {
// Path изменения менее критичны, используем больше throttling
if (!pathUpdatePending) {
pathUpdatePending = true;
uiHandler.removeCallbacks(pathUpdateRunnable);
uiHandler.postDelayed(pathUpdateRunnable, PATH_UPDATE_THROTTLE);
Log.d(TAG, "Path update запланирован на " + PATH_UPDATE_THROTTLE + "мс");
}
}
@Override
public void onRequestCenterMap(double latitude, double longitude) {
// Центрирование карты должно происходить немедленно
uiHandler.post(() -> {
if (mapInterface != null) {
mapInterface.centerOnPosition(latitude, longitude);
Log.d(TAG, "Карта отцентрирована на " + latitude + "," + longitude);
}
});
}
@Override
public void onCompassUpdate(float azimuth) {
// Компас не относится к карте, передаем в MainActivity через callback
Log.d(TAG, "Compass update: " + azimuth + "° - требует специальной обработки в MainActivity");
}
}