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
@@ -158,7 +158,7 @@ public class CoordinatesDockWidget extends BaseDockWidget {
testPaint.setTextSize(dp(16));
testPaint.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);
testPaint.setAntiAlias(true);
canvas.drawText("КООРДИНАТЫ", dp(16), dp(20), testPaint);
// canvas.drawText("КООРДИНАТЫ", dp(16), dp(20), testPaint);
// Рисуем текст
canvas.drawText(coordinatesText, dp(16), startY, coordinatesPaint);
@@ -0,0 +1,276 @@
package com.grigowashere.aismap.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.grigowashere.aismap.R;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
/**
* Overlay для отображения курсора на карте с координатами и информацией о расстоянии
*/
public class CursorOverlay {
private Context context;
private View overlayView;
private TextView tvCursorLatitude;
private TextView tvCursorLongitude;
private TextView tvDistance;
private TextView tvBearing;
private LinearLayout coordinatesPanel;
private LinearLayout distanceBearingPanel;
private LinearLayout aisVesselInfoPanel;
// AIS vessel info TextViews
private TextView tvAisMmsi;
private TextView tvAisName;
private TextView tvAisCallSign;
private TextView tvAisCog;
private TextView tvAisSog;
private Vessel ownVessel;
private AISVessel currentAisVessel;
private double cursorLatitude;
private double cursorLongitude;
public CursorOverlay(Context context) {
this.context = context;
initializeViews();
}
private void initializeViews() {
LayoutInflater inflater = LayoutInflater.from(context);
overlayView = inflater.inflate(R.layout.cursor, null);
tvCursorLatitude = overlayView.findViewById(R.id.tv_cursor_latitude);
tvCursorLongitude = overlayView.findViewById(R.id.tv_cursor_longitude);
tvDistance = overlayView.findViewById(R.id.tv_distance);
tvBearing = overlayView.findViewById(R.id.tv_bearing);
coordinatesPanel = overlayView.findViewById(R.id.coordinates_panel);
distanceBearingPanel = overlayView.findViewById(R.id.distance_bearing_panel);
aisVesselInfoPanel = overlayView.findViewById(R.id.ais_vessel_info_panel);
// Initialize AIS vessel info TextViews
tvAisMmsi = overlayView.findViewById(R.id.tv_ais_mmsi);
tvAisName = overlayView.findViewById(R.id.tv_ais_name);
tvAisCallSign = overlayView.findViewById(R.id.tv_ais_call_sign);
tvAisCog = overlayView.findViewById(R.id.tv_ais_cog);
tvAisSog = overlayView.findViewById(R.id.tv_ais_sog);
// По умолчанию курсор скрыт
overlayView.setVisibility(View.GONE);
}
public View getView() {
return overlayView;
}
/**
* Обновляет координаты курсора (центра экрана)
*/
public void updateCursorCoordinates(double latitude, double longitude) {
this.cursorLatitude = latitude;
this.cursorLongitude = longitude;
tvCursorLatitude.setText(String.format("%.6f°", latitude));
tvCursorLongitude.setText(String.format("%.6f°", longitude));
// Обновляем информацию о расстоянии и пеленге, если есть данные о нашем судне
updateDistanceAndBearing();
}
/**
* Устанавливает данные о нашем судне для расчета расстояния и пеленга
*/
public void setOwnVessel(Vessel vessel) {
this.ownVessel = vessel;
updateDistanceAndBearing();
}
/**
* Обновляет информацию о расстоянии и пеленге
*/
private void updateDistanceAndBearing() {
if (ownVessel != null && isValidPosition(ownVessel)) {
double distance = calculateDistance(
ownVessel.getLatitude(), ownVessel.getLongitude(),
cursorLatitude, cursorLongitude
);
// Вычисляем пеленг от судна к курсору
double bearingToCursor = calculateBearing(
ownVessel.getLatitude(), ownVessel.getLongitude(),
cursorLatitude, cursorLongitude
);
// Вычисляем относительный пеленг (на сколько градусов повернуть от курса судна)
double relativeBearing;
if (ownVessel.getCourse() > 0) {
// Пеленг относительно курса судна
relativeBearing = bearingToCursor - ownVessel.getCourse();
// Нормализуем в диапазон -180..+180
while (relativeBearing > 180) relativeBearing -= 360;
while (relativeBearing < -180) relativeBearing += 360;
} else {
// Если курс неизвестен, показываем абсолютный пеленг
relativeBearing = bearingToCursor;
}
// Форматируем расстояние: в км с дробной частью если > 1000м, иначе в метрах
String distanceText;
if (distance >= 1000) {
distanceText = String.format("Rng: %.2f км", distance / 1000.0);
} else {
distanceText = String.format("Rng: %.1f м", distance);
}
tvDistance.setText(distanceText);
tvBearing.setText(String.format("Brg: %.1f°", relativeBearing));
// Показываем информацию о расстоянии и пеленге
tvDistance.setVisibility(View.VISIBLE);
tvBearing.setVisibility(View.VISIBLE);
} else {
// Скрываем информацию, если нет валидных координат нашего судна
tvDistance.setVisibility(View.GONE);
tvBearing.setVisibility(View.GONE);
}
}
/**
* Вычисляет расстояние между двумя точками в метрах (формула гаверсинуса)
*/
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
final int R = 6371000; // Радиус Земли в метрах
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLatRad = Math.toRadians(lat2 - lat1);
double deltaLonRad = Math.toRadians(lon2 - lon1);
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
/**
* Вычисляет пеленг от первой точки ко второй в градусах
*/
private double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLonRad = Math.toRadians(lon2 - lon1);
double y = Math.sin(deltaLonRad) * Math.cos(lat2Rad);
double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLonRad);
double bearing = Math.toDegrees(Math.atan2(y, x));
return (bearing + 360) % 360; // Нормализуем в диапазон 0-360
}
/**
* Скрывает курсор
*/
public void hideCursor() {
if (overlayView != null) {
overlayView.setVisibility(View.GONE);
}
}
/**
* Показывает курсор
*/
public void showCursor() {
if (overlayView != null) {
overlayView.setVisibility(View.VISIBLE);
}
}
/**
* Устанавливает информацию об AIS судне под курсором
*/
public void setAisVesselInfo(AISVessel vessel) {
this.currentAisVessel = vessel;
updateAisVesselInfo();
}
/**
* Обновляет отображение информации об AIS судне
*/
private void updateAisVesselInfo() {
if (currentAisVessel != null) {
// MMSI
tvAisMmsi.setText("MMSI: " + currentAisVessel.getMmsi());
// Название
String name = currentAisVessel.getVesselName();
if (name != null && !name.trim().isEmpty()) {
tvAisName.setText("Название: " + name);
tvAisName.setVisibility(View.VISIBLE);
} else {
tvAisName.setVisibility(View.GONE);
}
// Позывной
String callSign = currentAisVessel.getCallSign();
if (callSign != null && !callSign.trim().isEmpty()) {
tvAisCallSign.setText("Позывной: " + callSign);
tvAisCallSign.setVisibility(View.VISIBLE);
} else {
tvAisCallSign.setVisibility(View.GONE);
}
// COG (курс)
if (currentAisVessel.getCourse() > 0) {
tvAisCog.setText(String.format("COG: %.1f°", currentAisVessel.getCourse()));
tvAisCog.setVisibility(View.VISIBLE);
} else {
tvAisCog.setVisibility(View.GONE);
}
// SOG (скорость)
if (currentAisVessel.getSpeed() > 0) {
tvAisSog.setText(String.format("SOG: %.1f уз", currentAisVessel.getSpeed()));
tvAisSog.setVisibility(View.VISIBLE);
} else {
tvAisSog.setVisibility(View.GONE);
}
// Показываем панель
aisVesselInfoPanel.setVisibility(View.VISIBLE);
} else {
// Скрываем панель
aisVesselInfoPanel.setVisibility(View.GONE);
}
}
/**
* Очищает информацию об AIS судне
*/
public void clearAisVesselInfo() {
this.currentAisVessel = null;
aisVesselInfoPanel.setVisibility(View.GONE);
}
/**
* Проверяет валидность позиции судна
*/
private boolean isValidPosition(Vessel vessel) {
if (vessel == null) return false;
double lat = vessel.getLatitude();
double lon = vessel.getLongitude();
// Проверяем, что координаты в допустимых пределах
return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180 &&
lat != 0.0 && lon != 0.0; // Исключаем нулевые координаты
}
}