generated from Grigo/AndroidTemplate
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:
@@ -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; // Исключаем нулевые координаты
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user