generated from Grigo/AndroidTemplate
b5aee265bc
Архитектурные улучшения: - Внедрен 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
381 lines
14 KiB
Java
381 lines
14 KiB
Java
package com.grigowashere.aismap.maps;
|
|
|
|
import android.graphics.Color;
|
|
import android.util.Log;
|
|
import com.yandex.mapkit.geometry.Point;
|
|
import com.yandex.mapkit.map.MapObjectCollection;
|
|
import com.yandex.mapkit.map.PolylineMapObject;
|
|
import com.yandex.mapkit.map.MapObject;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
|
|
|
/**
|
|
* Класс для отслеживания и отображения пути движения судна
|
|
* Отображает сплошную линию пройденного пути и прогнозируемое движение
|
|
*/
|
|
public class VesselPathTracker {
|
|
|
|
private static final String TAG = "VesselPathTracker";
|
|
private static final int MAX_PATH_POINTS = 100; // Максимальное количество точек в пути
|
|
private static final long MIN_TIME_BETWEEN_POINTS = 1000; // Минимальное время между точками (1 секунда)
|
|
private static final double MIN_DISTANCE_BETWEEN_POINTS = 10.0; // Минимальное расстояние между точками (10 метров)
|
|
|
|
private String vesselId;
|
|
private MapObjectCollection mapObjects;
|
|
private ConcurrentLinkedQueue<PathPoint> pathHistory;
|
|
private PolylineMapObject pathLine;
|
|
private PolylineMapObject predictionLine;
|
|
private long lastUpdateTime;
|
|
private Point lastPosition;
|
|
|
|
|
|
// Настройки отображения
|
|
private int pathColor = Color.CYAN;
|
|
private int predictionColor = Color.YELLOW;
|
|
private float pathWidth = 3.0f;
|
|
private float predictionWidth = 2.0f;
|
|
private boolean isEnabled = true;
|
|
|
|
/**
|
|
* Точка пути с временной меткой
|
|
*/
|
|
private static class PathPoint {
|
|
public final Point position;
|
|
public final long timestamp;
|
|
public final double speed;
|
|
public final double course;
|
|
|
|
public PathPoint(Point position, long timestamp, double speed, double course) {
|
|
this.position = position;
|
|
this.timestamp = timestamp;
|
|
this.speed = speed;
|
|
this.course = course;
|
|
}
|
|
}
|
|
|
|
public VesselPathTracker(String vesselId, MapObjectCollection mapObjects) {
|
|
this.vesselId = vesselId;
|
|
this.mapObjects = mapObjects;
|
|
this.pathHistory = new ConcurrentLinkedQueue<>();
|
|
this.lastUpdateTime = 0;
|
|
Log.d(TAG, "Created VesselPathTracker for vessel: " + vesselId + ", mapObjects: " + (mapObjects != null ? "not null" : "null"));
|
|
}
|
|
|
|
/**
|
|
* Обновляет путь судна новой позицией
|
|
*/
|
|
public void updatePosition(double latitude, double longitude, double speed, double course) {
|
|
if (!isEnabled) {
|
|
Log.d(TAG, "VesselPathTracker disabled for vessel: " + vesselId);
|
|
return;
|
|
}
|
|
|
|
long currentTime = System.currentTimeMillis();
|
|
Point newPosition = new Point(latitude, longitude);
|
|
|
|
Log.d(TAG, "updatePosition called for vessel: " + vesselId +
|
|
", lat: " + latitude + ", lon: " + longitude +
|
|
", speed: " + speed + ", course: " + course);
|
|
|
|
// Проверяем, нужно ли добавить новую точку
|
|
if (shouldAddPoint(newPosition, currentTime)) {
|
|
PathPoint newPoint = new PathPoint(newPosition, currentTime, speed, course);
|
|
pathHistory.offer(newPoint);
|
|
|
|
// Ограничиваем количество точек
|
|
while (pathHistory.size() > MAX_PATH_POINTS) {
|
|
pathHistory.poll();
|
|
}
|
|
|
|
lastPosition = newPosition;
|
|
lastUpdateTime = currentTime;
|
|
|
|
Log.d(TAG, "Added new point to path for vessel: " + vesselId +
|
|
", total points: " + pathHistory.size());
|
|
|
|
// Обновляем отображение пути
|
|
updatePathDisplay();
|
|
} else {
|
|
Log.d(TAG, "Point not added for vessel: " + vesselId +
|
|
" (time or distance filter)");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Проверяет, нужно ли добавить новую точку в путь
|
|
*/
|
|
private boolean shouldAddPoint(Point newPosition, long currentTime) {
|
|
// Проверяем время
|
|
if (currentTime - lastUpdateTime < MIN_TIME_BETWEEN_POINTS) {
|
|
return false;
|
|
}
|
|
|
|
// Проверяем расстояние
|
|
if (lastPosition != null) {
|
|
double distance = calculateDistance(lastPosition, newPosition);
|
|
if (distance < MIN_DISTANCE_BETWEEN_POINTS) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Обновляет отображение пути на карте
|
|
*/
|
|
private void updatePathDisplay() {
|
|
Log.d(TAG, "updatePathDisplay called for vessel: " + vesselId);
|
|
|
|
if (pathHistory.isEmpty()) {
|
|
Log.d(TAG, "Path history is empty for vessel: " + vesselId);
|
|
return;
|
|
}
|
|
if (mapObjects == null) {
|
|
Log.d(TAG, "MapObjects is null for vessel: " + vesselId);
|
|
return;
|
|
}
|
|
|
|
// Создаем список точек для пройденного пути
|
|
List<Point> pathPoints = new ArrayList<>();
|
|
for (PathPoint point : pathHistory) {
|
|
pathPoints.add(point.position);
|
|
}
|
|
|
|
Log.d(TAG, "Creating path line with " + pathPoints.size() + " points for vessel: " + vesselId);
|
|
|
|
// Удаляем старые линии
|
|
try {
|
|
if (pathLine != null) {
|
|
Log.d(TAG, "Removing old path line for vessel: " + vesselId);
|
|
mapObjects.remove(pathLine);
|
|
pathLine = null;
|
|
}
|
|
if (predictionLine != null) {
|
|
Log.d(TAG, "Removing old prediction line for vessel: " + vesselId);
|
|
mapObjects.remove(predictionLine);
|
|
predictionLine = null;
|
|
}
|
|
} catch (RuntimeException e) {
|
|
Log.e(TAG, "Error removing old lines for vessel: " + vesselId, e);
|
|
// Коллекция могла быть инвалидирована (weak_ptr expired). Прекращаем обновления.
|
|
isEnabled = false;
|
|
return;
|
|
}
|
|
|
|
// Создаем линию пройденного пути
|
|
if (pathPoints.size() > 1) {
|
|
try {
|
|
Log.d(TAG, "Adding new path line for vessel: " + vesselId);
|
|
pathLine = mapObjects.addPolyline(new com.yandex.mapkit.geometry.Polyline(pathPoints));
|
|
if (pathLine != null) {
|
|
pathLine.setStrokeColor(pathColor);
|
|
pathLine.setStrokeWidth(pathWidth);
|
|
Log.d(TAG, "Path line created successfully for vessel: " + vesselId +
|
|
", color: " + Integer.toHexString(pathColor) +
|
|
", width: " + pathWidth);
|
|
} else {
|
|
Log.e(TAG, "Failed to create path line for vessel: " + vesselId);
|
|
}
|
|
} catch (RuntimeException e) {
|
|
Log.e(TAG, "Error creating path line for vessel: " + vesselId, e);
|
|
isEnabled = false;
|
|
return;
|
|
}
|
|
} else {
|
|
Log.d(TAG, "Not enough points for path line for vessel: " + vesselId +
|
|
" (need at least 2, have " + pathPoints.size() + ")");
|
|
}
|
|
|
|
// Создаем линию прогнозируемого движения
|
|
createPredictionLine();
|
|
}
|
|
|
|
/**
|
|
* Создает линию прогнозируемого движения
|
|
*/
|
|
private void createPredictionLine() {
|
|
Log.d(TAG, "createPredictionLine called for vessel: " + vesselId);
|
|
|
|
if (pathHistory.isEmpty()) {
|
|
Log.d(TAG, "Path history is empty for prediction line for vessel: " + vesselId);
|
|
return;
|
|
}
|
|
if (mapObjects == null) {
|
|
Log.d(TAG, "MapObjects is null for prediction line for vessel: " + vesselId);
|
|
return;
|
|
}
|
|
|
|
// Получаем последнюю точку
|
|
PathPoint lastPoint = null;
|
|
for (PathPoint point : pathHistory) {
|
|
lastPoint = point;
|
|
}
|
|
|
|
if (lastPoint == null || lastPoint.speed <= 0) {
|
|
Log.d(TAG, "Cannot create prediction line for vessel: " + vesselId +
|
|
" (lastPoint: " + (lastPoint != null ? "not null" : "null") +
|
|
", speed: " + (lastPoint != null ? lastPoint.speed : "N/A") + ")");
|
|
return;
|
|
}
|
|
|
|
// Рассчитываем прогнозируемую позицию через 1 минуту
|
|
double predictionTimeMinutes = 1.0; // 1 минута
|
|
double predictionDistance = lastPoint.speed * predictionTimeMinutes * 60.0; // расстояние в метрах
|
|
|
|
Log.d(TAG, "Creating prediction line for vessel: " + vesselId +
|
|
", speed: " + lastPoint.speed +
|
|
", course: " + lastPoint.course +
|
|
", prediction distance: " + predictionDistance + "m");
|
|
|
|
// Конвертируем курс в радианы
|
|
double courseRad = Math.toRadians(lastPoint.course);
|
|
|
|
// Рассчитываем новую позицию
|
|
double earthRadius = 6371000; // радиус Земли в метрах
|
|
double lat1 = Math.toRadians(lastPoint.position.getLatitude());
|
|
double lon1 = Math.toRadians(lastPoint.position.getLongitude());
|
|
|
|
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(predictionDistance / earthRadius) +
|
|
Math.cos(lat1) * Math.sin(predictionDistance / earthRadius) * Math.cos(courseRad));
|
|
|
|
double lon2 = lon1 + Math.atan2(Math.sin(courseRad) * Math.sin(predictionDistance / earthRadius) * Math.cos(lat1),
|
|
Math.cos(predictionDistance / earthRadius) - Math.sin(lat1) * Math.sin(lat2));
|
|
|
|
Point predictionPoint = new Point(Math.toDegrees(lat2), Math.toDegrees(lon2));
|
|
|
|
// Создаем линию прогноза
|
|
List<Point> predictionPoints = new ArrayList<>();
|
|
predictionPoints.add(lastPoint.position);
|
|
predictionPoints.add(predictionPoint);
|
|
|
|
try {
|
|
Log.d(TAG, "Adding prediction line for vessel: " + vesselId);
|
|
predictionLine = mapObjects.addPolyline(new com.yandex.mapkit.geometry.Polyline(predictionPoints));
|
|
if (predictionLine != null) {
|
|
predictionLine.setStrokeColor(predictionColor);
|
|
predictionLine.setStrokeWidth(predictionWidth);
|
|
Log.d(TAG, "Prediction line created successfully for vessel: " + vesselId +
|
|
", color: " + Integer.toHexString(predictionColor) +
|
|
", width: " + predictionWidth);
|
|
} else {
|
|
Log.e(TAG, "Failed to create prediction line for vessel: " + vesselId);
|
|
}
|
|
} catch (RuntimeException e) {
|
|
Log.e(TAG, "Error creating prediction line for vessel: " + vesselId, e);
|
|
isEnabled = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Рассчитывает расстояние между двумя точками в метрах
|
|
*/
|
|
private double calculateDistance(Point point1, Point point2) {
|
|
double lat1 = Math.toRadians(point1.getLatitude());
|
|
double lon1 = Math.toRadians(point1.getLongitude());
|
|
double lat2 = Math.toRadians(point2.getLatitude());
|
|
double lon2 = Math.toRadians(point2.getLongitude());
|
|
|
|
double dlat = lat2 - lat1;
|
|
double dlon = lon2 - lon1;
|
|
|
|
double a = Math.sin(dlat / 2) * Math.sin(dlat / 2) +
|
|
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlon / 2) * Math.sin(dlon / 2);
|
|
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
return 6371000 * c; // радиус Земли в метрах
|
|
}
|
|
|
|
/**
|
|
* Очищает путь судна
|
|
*/
|
|
public void clearPath() {
|
|
try {
|
|
if (pathLine != null && mapObjects != null) {
|
|
mapObjects.remove(pathLine);
|
|
pathLine = null;
|
|
}
|
|
if (predictionLine != null && mapObjects != null) {
|
|
mapObjects.remove(predictionLine);
|
|
predictionLine = null;
|
|
}
|
|
} catch (RuntimeException ignored) {
|
|
// Игнорируем ошибки очистки при невалидной коллекции
|
|
}
|
|
|
|
|
|
pathHistory.clear();
|
|
lastPosition = null;
|
|
}
|
|
|
|
/**
|
|
* Удаляет трекер пути
|
|
*/
|
|
public void remove() {
|
|
clearPath();
|
|
}
|
|
|
|
/**
|
|
* Включает/выключает отображение пути
|
|
*/
|
|
public void setEnabled(boolean enabled) {
|
|
this.isEnabled = enabled;
|
|
if (!enabled) {
|
|
clearPath();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Устанавливает цвет пройденного пути
|
|
*/
|
|
public void setPathColor(int color) {
|
|
this.pathColor = color;
|
|
if (pathLine != null) {
|
|
pathLine.setStrokeColor(color);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Устанавливает цвет прогнозируемого пути
|
|
*/
|
|
public void setPredictionColor(int color) {
|
|
this.predictionColor = color;
|
|
if (predictionLine != null) {
|
|
predictionLine.setStrokeColor(color);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Устанавливает ширину линий
|
|
*/
|
|
public void setLineWidth(float pathWidth, float predictionWidth) {
|
|
this.pathWidth = pathWidth;
|
|
this.predictionWidth = predictionWidth;
|
|
|
|
if (pathLine != null) {
|
|
pathLine.setStrokeWidth(pathWidth);
|
|
}
|
|
if (predictionLine != null) {
|
|
predictionLine.setStrokeWidth(predictionWidth);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Проверяет, активен ли трекер
|
|
*/
|
|
public boolean isActive() {
|
|
return isEnabled && !pathHistory.isEmpty();
|
|
}
|
|
|
|
/**
|
|
* Получает количество точек в пути
|
|
*/
|
|
public int getPathPointCount() {
|
|
return pathHistory.size();
|
|
}
|
|
}
|