feat: масштабный рефакторинг архитектуры AIS карты и UI компонентов

Новые векторные иконки:
- cog.xml: иконка шестеренки для настроек
- compass.xml: иконка компаса для навигации
- ownship.xml: иконка собственного судна
- targetlist.xml: иконка списка целей с текстом 'LIST'

Архитектурные изменения:
- MainActivity.java: +99/- строк - обновление UI логики
- AppController.java: +111/- строк - рефакторинг контроллера приложения
- MapLibreMapImpl.java: +525/- строк - значительные улучшения карты
- MapInterface.java: +10 строк - расширение интерфейса карты
- CursorOverlay.java: +329/- строк - улучшение курсора и оверлеев
- GeoUtils.java: +92 строк - новые гео-утилиты
- NavigationUtils.java: +81/- строк - оптимизация навигации
- VesselPathTracker.java: +18/- строк - улучшение трекинга судов
- MapForgeImpl.java, YandexMapImpl.java: обновления карт

UI изменения:
- activity_main.xml: +65/- строк - обновление главного layout
- cursor.xml: +16/- строк - улучшение курсора
- targetlist.xml: +39 строк - обновление иконки списка целей

Общий объем: +1087/-328 строк
Подготовка к новой архитектуре UI и картографических компонентов
This commit is contained in:
2025-10-06 08:33:13 +03:00
parent 932ca5f05f
commit 982e940b8d
23 changed files with 1680 additions and 329 deletions
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66.46 176.77">
<defs>
<style>
.cls-1 {
fill: silver;
stroke: #f3f3f3;
}
.cls-1, .cls-2 {
stroke-miterlimit: 10;
}
.cls-2 {
fill: #e32636;
stroke: #961923;
}
</style>
</defs>
<g id="_Слой_24" data-name="Слой_24">
<path class="cls-2" d="M33.73,64.39c12.24,0,22.33,9.16,23.81,21h8.19L33.23,1.39.73,85.39h9.19c1.48-11.84,11.57-21,23.81-21Z"/>
<path class="cls-1" d="M57.04,91.39c-1.48,11.84-11.57,21-23.81,21s-22.33-9.16-23.81-21H.73l32.5,84,32.5-84h-8.69Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 709 B

@@ -28,6 +28,7 @@ import com.grigowashere.aismap.controllers.AppController;
import com.grigowashere.aismap.controllers.MapController;
import com.grigowashere.aismap.controllers.VesselPathController;
import com.grigowashere.aismap.maps.MapInterface;
import com.grigowashere.aismap.maps.MapLibreMapImpl;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.sensors.CompassSensor;
@@ -62,10 +63,10 @@ public class MainActivity extends AppCompatActivity {
private MapView mapView;
private SettingsManager settingsManager;
private Button btnCenterOnVessel;
private Button btnMapOrientation;
private Button btnSettings;
private Button btnAisTargets;
private ImageButton btnCenterOnVessel;
private ImageButton btnMapOrientation;
private ImageButton btnSettings;
private ImageButton btnAisTargets;
private LinearLayout controlPanel;
private CompassView compassView;
private CompassSensor compassSensor;
@@ -75,6 +76,7 @@ public class MainActivity extends AppCompatActivity {
private android.os.Handler uiThrottleHandler;
private Runnable compassUpdateRunnable;
private Runnable coordinatesUpdateRunnable;
private Runnable compassButtonRotationRunnable;
private Vessel lastCompassVessel;
private Vessel lastCoordinatesVessel;
private static final long UI_UPDATE_THROTTLE_MS = 200; // 5 FPS максимум
@@ -174,6 +176,20 @@ public class MainActivity extends AppCompatActivity {
coordinatesWidget.updateVessel(lastCoordinatesVessel);
}
};
// Периодическое обновление поворота кнопки компаса по bearing карты
compassButtonRotationRunnable = () -> {
try {
if (btnMapOrientation != null && mapInterface != null) {
float bearing = mapInterface.getBearing();
// Иконка должна указывать север: вращаем противоположно bearing карты
btnMapOrientation.setRotation(-bearing);
}
} catch (Exception ignore) {}
// Планируем следующее обновление
if (uiThrottleHandler != null) {
uiThrottleHandler.postDelayed(compassButtonRotationRunnable, UI_UPDATE_THROTTLE_MS);
}
};
tvGpsAge = findViewById(R.id.tv_gps_age);
tvAisAge = findViewById(R.id.tv_ais_age);
@@ -191,12 +207,10 @@ public class MainActivity extends AppCompatActivity {
}
private void setupButtonListeners() {
btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
btnSettings.setOnClickListener(v -> showSettings());
if (btnAisTargets != null) {
btnAisTargets.setOnClickListener(v -> openAisTargets());
}
if (btnCenterOnVessel != null) btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
if (btnMapOrientation != null) btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
if (btnSettings != null) btnSettings.setOnClickListener(v -> showSettings());
if (btnAisTargets != null) btnAisTargets.setOnClickListener(v -> openAisTargets());
// Кнопка для показа информации о судне
// Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info);
@@ -277,6 +291,11 @@ public class MainActivity extends AppCompatActivity {
compassView.post(() -> {
updateControlPanelPosition();
});
// Стартуем обновление поворота кнопки компаса
if (uiThrottleHandler != null) {
uiThrottleHandler.removeCallbacks(compassButtonRotationRunnable);
uiThrottleHandler.post(compassButtonRotationRunnable);
}
}
private void setupCoordinatesWidget() {
@@ -848,9 +867,20 @@ public class MainActivity extends AppCompatActivity {
}
private void toggleMapOrientation() {
// TODO: Реализовать переключение ориентации карты
// Состояния: север, курс, компас
Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show();
if (mapInterface == null) return;
try {
float current = mapInterface.getBearing();
// Простейший toggle: если близко к северу — повернуть на 45°, иначе выровнять по северу
if (Math.abs(current) < 1f) {
mapInterface.setBearing(45f);
Toast.makeText(this, "Ориентация: произвольная (45°)", Toast.LENGTH_SHORT).show();
} else {
mapInterface.setBearing(0f);
Toast.makeText(this, "Ориентация: север вверх", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.w(TAG, "toggleMapOrientation error: " + e.getMessage());
}
}
private void togglePathTracking() {
@@ -1058,6 +1088,11 @@ public class MainActivity extends AppCompatActivity {
mapInterface.initialize();
Log.i(TAG, "Карта инициализирована");
// Обновляем размеры экрана для курсора после инициализации
if (mapInterface instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapInterface).updateScreenDimensions();
}
// Применяем отложенное центрирование, если было
applyPendingCenterIfAny();
@@ -1081,6 +1116,21 @@ public class MainActivity extends AppCompatActivity {
// Обрабатываем возможный интент центрирования
handleCenterIntentIfAny(getIntent());
// Восстанавливаем курсор после возврата в активность
if (mapInterface != null) {
boolean cursorEnabled = settingsManager.isCursorEnabled();
if (cursorEnabled) {
mapInterface.showCursor();
// Обновляем координаты курсора с центра карты
mapInterface.updateCursorFromMapCenter();
// Принудительно проверяем AIS судно под курсором для восстановления панели
if (mapInterface instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapInterface).forceCheckAisVesselUnderCursor();
}
}
}
// Проверяем разрешения и запускаем контроллеры
checkPermissions();
}
@@ -1126,6 +1176,16 @@ public class MainActivity extends AppCompatActivity {
}
}
@Override
protected void onPause() {
super.onPause();
// Очищаем информацию о AIS судне при паузе активности
if (mapInterface != null) {
mapInterface.clearAisVesselInfo();
}
}
@Override
protected void onStop() {
super.onStop();
@@ -1137,6 +1197,8 @@ public class MainActivity extends AppCompatActivity {
// Останавливаем карту
if (mapInterface != null) {
// Очищаем информацию о AIS судне перед остановкой карты
mapInterface.clearAisVesselInfo();
mapInterface.cleanup();
}
@@ -1149,6 +1211,7 @@ public class MainActivity extends AppCompatActivity {
if (uiThrottleHandler != null) {
uiThrottleHandler.removeCallbacks(compassUpdateRunnable);
uiThrottleHandler.removeCallbacks(coordinatesUpdateRunnable);
uiThrottleHandler.removeCallbacks(compassButtonRotationRunnable);
}
// Не останавливаем слушатели здесь, чтобы UDP продолжал работать в фоне
@@ -1166,6 +1229,11 @@ public class MainActivity extends AppCompatActivity {
mapView.onDestroy();
}
// Очищаем информацию о AIS судне при уничтожении активности
if (mapInterface != null) {
mapInterface.clearAisVesselInfo();
}
// Останавливаем обновление времени
stopTimeUpdate();
@@ -1219,7 +1287,10 @@ public class MainActivity extends AppCompatActivity {
// Обрабатываем изменения конфигурации (например, поворот экрана)
if (mapInterface != null) {
// Можно добавить логику для обработки изменений конфигурации карты
// Обновляем размеры экрана для курсора после поворота
if (mapInterface instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapInterface).updateScreenDimensions();
}
}
}
@@ -211,10 +211,12 @@ public class AppController implements
// Восстанавливаем AIS суда
if (aisVessels != null && !aisVessels.isEmpty()) {
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
synchronized (aisVessels) {
for (AISVessel v : aisVessels) {
Log.d(TAG, " - AIS судно: " + v.getMmsi() + " на " + v.getLatitude() + "," + v.getLongitude());
uiDataNotifier.onAISVesselChanged(v);
}
}
Log.i(TAG, "" + aisVessels.size() + " AIS судов отправлено в UI Coordinator");
} else {
Log.i(TAG, "ℹ️ Нет AIS судов для восстановления");
@@ -255,10 +257,14 @@ public class AppController implements
}
// UDP слушатель запускается в фоновом потоке
if (isUDPEnabled) {
if (isUDPEnabled && executor != null && !executor.isShutdown()) {
try {
executor.execute(() -> {
udpListener.start();
});
} catch (java.util.concurrent.RejectedExecutionException e) {
Log.w(TAG, "Thread pool is shutting down, cannot start UDP listener: " + e.getMessage());
}
}
// Запускаем периодическую очистку БД от устаревших AIS целей
@@ -570,7 +576,9 @@ public class AppController implements
}
} else {
// Добавляем новое судно
synchronized (aisVessels) {
aisVessels.add(vessel);
}
// Если это новое судно сразу пришло с safety-сообщением — уведомим
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
@@ -661,6 +669,8 @@ public class AppController implements
}
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
if (executor != null && !executor.isShutdown()) {
try {
executor.execute(() -> {
try {
nmeaParser.parseNMEA(data);
@@ -674,6 +684,12 @@ public class AppController implements
Log.e(TAG, "❌ Ошибка парсинга UDP NMEA в фоновом потоке: " + e.getMessage(), e);
}
});
} catch (java.util.concurrent.RejectedExecutionException e) {
Log.w(TAG, "Thread pool is shutting down, skipping UDP data processing: " + e.getMessage());
}
} else {
Log.w(TAG, "Thread pool is not available, skipping UDP data processing");
}
// Обновляем метки времени по префиксу в UI потоке (быстрая операция)
updateLastMessageAgesFromRaw(data);
@@ -701,6 +717,8 @@ public class AppController implements
}
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
if (executor != null && !executor.isShutdown()) {
try {
executor.execute(() -> {
try {
nmeaParser.parseNMEA(message);
@@ -714,6 +732,12 @@ public class AppController implements
Log.e(TAG, "❌ Ошибка парсинга NMEA в фоновом потоке: " + e.getMessage(), e);
}
});
} catch (java.util.concurrent.RejectedExecutionException e) {
Log.w(TAG, "Thread pool is shutting down, skipping NMEA processing: " + e.getMessage());
}
} else {
Log.w(TAG, "Thread pool is not available, skipping NMEA processing");
}
// Обновляем метки времени в UI потоке (быстрая операция)
if (message != null) {
@@ -765,11 +789,13 @@ public class AppController implements
* Находит AIS судно по MMSI
*/
private AISVessel findAISVesselByMMSI(String mmsi) {
synchronized (aisVessels) {
for (AISVessel vessel : aisVessels) {
if (mmsi.equals(vessel.getMmsi())) {
return vessel;
}
}
}
return null;
}
@@ -784,8 +810,10 @@ public class AppController implements
* Получает список AIS судов
*/
public List<AISVessel> getAISVessels() {
synchronized (aisVessels) {
return new ArrayList<>(aisVessels);
}
}
/**
* Очищает все AIS суда
@@ -794,7 +822,9 @@ public class AppController implements
Log.i(TAG, "Очищаем AIS суда из контроллера");
// Очищаем локальные данные
synchronized (aisVessels) {
aisVessels.clear();
}
// Уведомляем UI Coordinator о необходимости очистки карты
if (uiDataNotifier != null) {
@@ -900,6 +930,17 @@ public class AppController implements
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
try {
// Ждем завершения всех задач максимум 2 секунды
if (!executor.awaitTermination(2, java.util.concurrent.TimeUnit.SECONDS)) {
Log.w(TAG, "Thread pool did not terminate gracefully, forcing shutdown");
executor.shutdownNow();
}
} catch (InterruptedException e) {
Log.w(TAG, "Thread pool shutdown interrupted: " + e.getMessage());
executor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
@@ -155,6 +155,17 @@ public class MapForgeImpl implements MapInterface {
return mapView.getModel().mapViewPosition.getZoomLevel();
}
@Override
public void setBearing(float bearing) {
// MapForge: нет прямой поддержки bearing у MapViewPosition — игнорируем
}
@Override
public float getBearing() {
// MapForge: возвращаем всегда север вверх
return 0f;
}
@Override
public void addLayer(String layerId, Object layerData) {
// Реализация добавления слоев для MapForge
@@ -64,6 +64,16 @@ public interface MapInterface {
*/
float getZoom();
/**
* Установка курса (bearing) карты в градусах (0 = север вверх)
*/
void setBearing(float bearing);
/**
* Текущий курс (bearing) карты в градусах
*/
float getBearing();
/**
* Добавление дополнительного слоя
*/
@@ -10,6 +10,7 @@ import android.util.Log;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.utils.SettingsManager;
import com.grigowashere.aismap.utils.GeoUtils;
import com.grigowashere.aismap.controllers.VesselPathController;
import com.grigowashere.aismap.controllers.AppController;
import com.grigowashere.aismap.view.CursorOverlay;
@@ -50,7 +51,7 @@ public class MapLibreMapImpl implements MapInterface {
private static final String LAYER_AIS_PATHS = "ais_paths_layer";
private static final String SOURCE_AIS_PREDICTIONS = "ais_predictions_source";
private static final String LAYER_AIS_PREDICTIONS = "ais_predictions_layer";
private static final String IMAGE_VESSEL_OWN = "vessel_icon_own";
private static final String IMAGE_VESSEL_OWN = "ownship";
private static final String IMAGE_VESSEL_A = "vessel_icon_a";
private static final String IMAGE_VESSEL_B = "vessel_icon_b";
// Имиджи, сопоставленные с ресурсами drawable (target_*.xml/png)
@@ -81,6 +82,10 @@ public class MapLibreMapImpl implements MapInterface {
private AppController appController; // Для доступа к AIS VesselPathController
private CursorOverlay cursorOverlay;
private Vessel ownVessel;
// Отладка
private boolean debugMode = true; // Включаем отладку по умолчанию
private android.graphics.RectF lastSearchRect = null;
private final android.os.Handler uiHandler = new android.os.Handler(android.os.Looper.getMainLooper());
private final android.os.Handler staleHandler = new android.os.Handler(android.os.Looper.getMainLooper());
@@ -249,6 +254,19 @@ public class MapLibreMapImpl implements MapInterface {
refreshGeoJson();
setupClickListener();
setupMapMovementListener();
// Устанавливаем размеры экрана для курсора после инициализации карты
if (cursorOverlay != null && mapView != null) {
mapView.post(() -> {
int width = mapView.getWidth();
int height = mapView.getHeight();
if (width > 0 && height > 0) {
cursorOverlay.setScreenDimensions(width, height);
Log.d(TAG, String.format("Установлены размеры экрана для курсора: %dx%d", width, height));
}
});
}
staleHandler.removeCallbacks(staleRunnable);
staleHandler.postDelayed(staleRunnable, 5_000L);
});
@@ -378,7 +396,7 @@ public class MapLibreMapImpl implements MapInterface {
if (vessel == null || vessel.getMmsi() == null) return;
// Проверяем валидность координат
if (!isValidCoordinates(vessel.getLatitude(), vessel.getLongitude())) {
if (!GeoUtils.isValidCoordinates(vessel.getLatitude(), vessel.getLongitude())) {
Log.d(TAG, "updateAISVesselPosition: AIS vessel " + vessel.getMmsi() +
" has invalid coordinates " + vessel.getLatitude() + "," + vessel.getLongitude() +
" - skipping marker and path update");
@@ -492,7 +510,8 @@ public class MapLibreMapImpl implements MapInterface {
@Override
public void setZoom(float zoom) {
if (maplibreMap == null) return;
if (maplibreMap == null || mapView == null) return;
try {
org.maplibre.android.camera.CameraPosition current = maplibreMap.getCameraPosition();
maplibreMap.setCameraPosition(new org.maplibre.android.camera.CameraPosition.Builder()
.target(current.target)
@@ -500,12 +519,47 @@ public class MapLibreMapImpl implements MapInterface {
.tilt(current.tilt)
.bearing(current.bearing)
.build());
} catch (Exception e) {
Log.w(TAG, "setZoom: MapView may be destroyed: " + e.getMessage());
}
}
@Override
public void setBearing(float bearing) {
if (maplibreMap == null || mapView == null) return;
try {
org.maplibre.android.camera.CameraPosition current = maplibreMap.getCameraPosition();
maplibreMap.setCameraPosition(new org.maplibre.android.camera.CameraPosition.Builder()
.target(current.target)
.zoom((float) current.zoom)
.tilt(current.tilt)
.bearing(bearing)
.build());
} catch (Exception e) {
Log.w(TAG, "setBearing: MapView may be destroyed: " + e.getMessage());
}
}
@Override
public float getBearing() {
if (maplibreMap == null) return 0f;
try {
return (float) maplibreMap.getCameraPosition().bearing;
} catch (Exception e) {
Log.w(TAG, "getBearing: MapView may be destroyed: " + e.getMessage());
return 0f;
}
}
@Override
public float getZoom() {
if (maplibreMap == null) return 0f;
if (maplibreMap == null || mapView == null) return 0f;
try {
return (float) maplibreMap.getCameraPosition().zoom;
} catch (Exception e) {
Log.w(TAG, "getZoom: MapView may be destroyed: " + e.getMessage());
return 0f;
}
}
@Override
@@ -533,10 +587,10 @@ public class MapLibreMapImpl implements MapInterface {
// Иконки судов: own, class A, class B
try {
if (style.getImage(IMAGE_VESSEL_OWN) == null) {
Bitmap bmpOwn = getBitmapByName("target");
Bitmap bmpOwn = getBitmapByName("ownship");
if (bmpOwn == null) bmpOwn = BitmapFactory.decodeResource(context.getResources(), android.R.drawable.ic_menu_compass);
// Добавляем как SDF для последующего окрашивания iconColor
style.addImage(IMAGE_VESSEL_OWN, bmpOwn, true);
// Добавляем без SDF для сохранения цвета
style.addImage(IMAGE_VESSEL_OWN, bmpOwn, false);
}
// Предзагрузка цветных иконок из ресурсов target_a_* и target_b_*
preloadClassTypeIcons("a");
@@ -955,8 +1009,8 @@ public class MapLibreMapImpl implements MapInterface {
JSONObject props = new JSONObject();
props.put("course", normalizeCourse(course));
props.put("own", own);
// Для собственного судна используем дефолтную иконку типа (серый), чтобы точно существовала
props.put("icon", own ? "target_a_other" : "target_b_other");
// Для собственного судна используем ownship иконку, для AIS - дефолтную
props.put("icon", own ? IMAGE_VESSEL_OWN : "target_b_other");
feature.put("properties", props);
return feature;
@@ -972,10 +1026,7 @@ public class MapLibreMapImpl implements MapInterface {
}
private double normalizeCourse(double c) {
if (Double.isNaN(c) || Double.isInfinite(c)) return 0.0;
double v = c % 360.0;
if (v < 0) v += 360.0;
return v;
return GeoUtils.normalizeAngle(c);
}
private String emptyFeatureCollection() {
@@ -986,32 +1037,6 @@ public class MapLibreMapImpl implements MapInterface {
return "{\"type\":\"Feature\",\"geometry\":{\"type\":\"LineString\",\"coordinates\":[[0,0],[0,0]]}}";
}
/**
* Проверяет валидность координат
* Игнорирует координаты 0,0 и 181,91 (невалидные значения AIS)
*/
private boolean isValidCoordinates(double latitude, double longitude) {
// Проверяем на нулевые координаты
if (latitude == 0.0 && longitude == 0.0) {
return false;
}
// Проверяем на невалидные координаты AIS (181, 91)
if (latitude == 91.0 && longitude == 181.0) {
return false;
}
// Проверяем на стандартные границы координат
if (latitude < -90.0 || latitude > 90.0) {
return false;
}
if (longitude < -180.0 || longitude > 180.0) {
return false;
}
return true;
}
private void appendOwnPathPoint(double lon, double lat) {
try {
@@ -1322,22 +1347,78 @@ public class MapLibreMapImpl implements MapInterface {
private final MapLibreMap.OnMapClickListener onMapClickListener = point -> {
if (maplibreMap == null || style == null) return false;
try {
// Кликаем по слою
// Получаем экранные координаты клика
android.graphics.PointF screenPoint = maplibreMap.getProjection().toScreenLocation(point);
// Адаптивный радиус поиска для кликов (немного меньше чем для курсора)
double zoom;
try {
zoom = maplibreMap.getCameraPosition().zoom;
} catch (Exception e) {
Log.w(TAG, "onMapClickListener: MapView may be destroyed: " + e.getMessage());
return false;
}
float pixelRadius = calculateAdaptivePixelRadius(zoom) * 0.8f;
// Ищем суда в области клика
java.util.List<org.maplibre.geojson.Feature> features = maplibreMap.queryRenderedFeatures(
maplibreMap.getProjection().toScreenLocation(point), LAYER_VESSELS);
new android.graphics.RectF(
screenPoint.x - pixelRadius, screenPoint.y - pixelRadius,
screenPoint.x + pixelRadius, screenPoint.y + pixelRadius
), LAYER_VESSELS);
if (features != null && !features.isEmpty()) {
String id = features.get(0).id();
if ("own_vessel".equals(id)) {
// Находим ближайшее судно к точке клика
String closestId = null;
double minDistance = Double.MAX_VALUE;
for (org.maplibre.geojson.Feature feature : features) {
String id = feature.id();
if (id != null) {
// Получаем координаты судна
org.maplibre.android.geometry.LatLng vesselLatLng;
if ("own_vessel".equals(id) && lastOwnVessel != null) {
vesselLatLng = new org.maplibre.android.geometry.LatLng(
lastOwnVessel.getLatitude(), lastOwnVessel.getLongitude());
} else {
AISVessel vessel = idToAisVessel.get(id);
if (vessel != null) {
vesselLatLng = new org.maplibre.android.geometry.LatLng(
vessel.getLatitude(), vessel.getLongitude());
} else {
continue;
}
}
// Вычисляем экранное расстояние
android.graphics.PointF vesselScreenPoint = maplibreMap.getProjection()
.toScreenLocation(vesselLatLng);
double distance = Math.sqrt(
Math.pow(screenPoint.x - vesselScreenPoint.x, 2) +
Math.pow(screenPoint.y - vesselScreenPoint.y, 2)
);
if (distance < minDistance) {
minDistance = distance;
closestId = id;
}
}
}
// Обрабатываем клик по ближайшему судну
if (closestId != null && minDistance <= pixelRadius) {
if ("own_vessel".equals(closestId)) {
if (markerClickListener != null) {
markerClickListener.onOwnVesselClick(lastOwnVessel);
}
} else {
if (markerClickListener != null) {
markerClickListener.onAISVesselClick(idToAisVessel.get(id));
markerClickListener.onAISVesselClick(idToAisVessel.get(closestId));
}
}
return true;
}
}
} catch (Exception ignored) {}
return false;
};
@@ -1705,13 +1786,24 @@ public class MapLibreMapImpl implements MapInterface {
return false;
}
// Попытка обращения к состоянию стиля для проверки валидности
style.isFullyLoaded();
// Проверяем, что стиль полностью загружен
if (!style.isFullyLoaded()) {
return false;
}
// Дополнительная проверка - пытаемся получить доступ к методам стиля
// Это поможет выявить состояние "newer style is loading"
style.getSources();
// Если мы дошли до этого места, стиль валиден
return true;
} catch (IllegalStateException e) {
// Это именно та ошибка, которую мы ловим
Log.d(TAG, "isStyleValid: стиль в процессе загрузки: " + e.getMessage());
return false;
} catch (Exception e) {
// Если произошло исключение, стиль не валиден
// Если произошло другое исключение, стиль не валиден
Log.d(TAG, "isStyleValid: ошибка проверки стиля: " + e.getMessage());
return false;
}
}
@@ -1739,13 +1831,22 @@ public class MapLibreMapImpl implements MapInterface {
@Override
public void updateCursorFromMapCenter() {
if (cursorOverlay != null && maplibreMap != null) {
if (cursorOverlay != null && maplibreMap != null && mapView != null) {
try {
// Получаем координаты центра карты
org.maplibre.android.geometry.LatLng center = maplibreMap.getCameraPosition().target;
Log.d(TAG, String.format("updateCursorFromMapCenter: center=%.6f,%.6f",
center.getLatitude(), center.getLongitude()));
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
// Проверяем, есть ли AIS судно под курсором
checkAisVesselUnderCursor(center);
} catch (Exception e) {
Log.w(TAG, "updateCursorFromMapCenter: MapView may be destroyed: " + e.getMessage());
}
} else {
Log.d(TAG, "updateCursorFromMapCenter: cursorOverlay, maplibreMap или mapView равны null");
}
}
@@ -1753,88 +1854,322 @@ public class MapLibreMapImpl implements MapInterface {
* Проверяет, есть ли AIS судно под курсором (в центре экрана)
*/
private void checkAisVesselUnderCursor(org.maplibre.android.geometry.LatLng center) {
if (maplibreMap == null || style == null) return;
if (maplibreMap == null || style == null) {
Log.d(TAG, "checkAisVesselUnderCursor: maplibreMap или style равны null");
return;
}
// Проверяем, что стиль готов к работе
if (!isStyleValid()) {
Log.d(TAG, "checkAisVesselUnderCursor: стиль не готов, пропускаем поиск");
return;
}
try {
// Получаем экранные координаты центра
android.graphics.PointF screenPoint = maplibreMap.getProjection().toScreenLocation(center);
// Ищем AIS суда в радиусе 50 пикселей от центра
java.util.List<org.maplibre.geojson.Feature> features = maplibreMap.queryRenderedFeatures(
new android.graphics.RectF(
screenPoint.x - 50, screenPoint.y - 50,
screenPoint.x + 50, screenPoint.y + 50
), LAYER_VESSELS);
// Адаптивный радиус поиска в зависимости от масштаба карты
double zoom;
try {
zoom = maplibreMap.getCameraPosition().zoom;
} catch (Exception e) {
Log.w(TAG, "checkAisVesselUnderCursor: MapView may be destroyed: " + e.getMessage());
return;
}
float pixelRadius = calculateAdaptivePixelRadius(zoom);
Log.d(TAG, String.format("checkAisVesselUnderCursor: center=%.6f,%.6f, screen=%.1f,%.1f, zoom=%.2f, radius=%.1f",
center.getLatitude(), center.getLongitude(), screenPoint.x, screenPoint.y, zoom, pixelRadius));
// Создаем область поиска
android.graphics.RectF searchRect = new android.graphics.RectF(
screenPoint.x - pixelRadius, screenPoint.y - pixelRadius,
screenPoint.x + pixelRadius, screenPoint.y + pixelRadius
);
// Сохраняем для отладочной визуализации
if (debugMode) {
lastSearchRect = new android.graphics.RectF(searchRect);
updateDebugVisualization();
}
Log.d(TAG, String.format("checkAisVesselUnderCursor: searchRect=[%.1f,%.1f,%.1f,%.1f]",
searchRect.left, searchRect.top, searchRect.right, searchRect.bottom));
// Ищем AIS суда в адаптивном радиусе от центра
java.util.List<org.maplibre.geojson.Feature> features = maplibreMap.queryRenderedFeatures(searchRect, LAYER_VESSELS);
Log.d(TAG, String.format("checkAisVesselUnderCursor: найдено %d features в основном поиске",
features != null ? features.size() : 0));
// Если не нашли в основном радиусе, попробуем расширенный поиск
if ((features == null || features.isEmpty()) && pixelRadius < 150) {
android.graphics.RectF expandedRect = new android.graphics.RectF(
screenPoint.x - 150, screenPoint.y - 150,
screenPoint.x + 150, screenPoint.y + 150
);
features = maplibreMap.queryRenderedFeatures(expandedRect, LAYER_VESSELS);
Log.d(TAG, String.format("checkAisVesselUnderCursor: найдено %d features в расширенном поиске",
features != null ? features.size() : 0));
}
if (features != null && !features.isEmpty()) {
// Находим ближайшее AIS судно
Log.d(TAG, "checkAisVesselUnderCursor: обрабатываем найденные features");
// Находим ближайшее AIS судно с учетом как экранного, так и географического расстояния
AISVessel closestVessel = null;
double minDistance = Double.MAX_VALUE;
double minScreenDistance = Double.MAX_VALUE;
double minGeoDistance = Double.MAX_VALUE;
for (org.maplibre.geojson.Feature feature : features) {
String id = feature.id();
Log.d(TAG, String.format("checkAisVesselUnderCursor: проверяем feature с id=%s", id));
if (id != null && !"own_vessel".equals(id)) {
AISVessel vessel = idToAisVessel.get(id);
if (vessel != null) {
// Вычисляем расстояние от центра до судна
double distance = calculateDistance(
// Вычисляем географическое расстояние от центра до судна
double geoDistance = GeoUtils.calculateDistance(
center.getLatitude(), center.getLongitude(),
vessel.getLatitude(), vessel.getLongitude()
);
if (distance < minDistance) {
minDistance = distance;
// Вычисляем экранное расстояние
android.graphics.PointF vesselScreenPoint = maplibreMap.getProjection()
.toScreenLocation(new org.maplibre.android.geometry.LatLng(
vessel.getLatitude(), vessel.getLongitude()));
double screenDistance = Math.sqrt(
Math.pow(screenPoint.x - vesselScreenPoint.x, 2) +
Math.pow(screenPoint.y - vesselScreenPoint.y, 2)
);
Log.d(TAG, String.format("checkAisVesselUnderCursor: судно %s - geoDistance=%.1f м, screenDistance=%.1f пикс",
id, geoDistance, screenDistance));
// Приоритет отдаем экранному расстоянию, но учитываем и географическое
boolean isBetterCandidate = false;
if (closestVessel == null) {
isBetterCandidate = true;
} else if (screenDistance < minScreenDistance * 0.8) {
// Если экранное расстояние значительно меньше
isBetterCandidate = true;
} else if (screenDistance <= minScreenDistance * 1.2 && geoDistance < minGeoDistance) {
// Если экранное расстояние примерно равно, но географическое меньше
isBetterCandidate = true;
}
if (isBetterCandidate) {
Log.d(TAG, String.format("checkAisVesselUnderCursor: выбираем судно %s как лучший кандидат", id));
minScreenDistance = screenDistance;
minGeoDistance = geoDistance;
closestVessel = vessel;
}
} else {
Log.d(TAG, String.format("checkAisVesselUnderCursor: судно с id=%s не найдено в idToAisVessel", id));
}
} else if ("own_vessel".equals(id)) {
Log.d(TAG, "checkAisVesselUnderCursor: пропускаем собственное судно");
}
}
// Если нашли судно в радиусе 100 метров, показываем информацию
if (closestVessel != null && minDistance < 100) {
// Адаптивный порог для географического расстояния в зависимости от масштаба
double maxGeoDistance = calculateAdaptiveGeoRadius(zoom);
Log.d(TAG, String.format("checkAisVesselUnderCursor: лучший кандидат - screenDistance=%.1f (лимит %.1f), geoDistance=%.1f (лимит %.1f)",
minScreenDistance, pixelRadius * 1.5, minGeoDistance, maxGeoDistance));
// Если нашли судно в допустимом радиусе, показываем информацию
if (closestVessel != null &&
(minScreenDistance <= pixelRadius * 1.5 || minGeoDistance <= maxGeoDistance)) {
Log.d(TAG, String.format("checkAisVesselUnderCursor: показываем информацию о судне %s", closestVessel.getMmsi()));
setAisVesselInfo(closestVessel);
} else {
Log.d(TAG, "checkAisVesselUnderCursor: судно не прошло проверку расстояния, очищаем информацию");
clearAisVesselInfo();
}
} else {
Log.d(TAG, "checkAisVesselUnderCursor: features пустой, очищаем информацию");
clearAisVesselInfo();
}
} catch (Exception e) {
// В случае ошибки очищаем информацию
Log.e(TAG, "checkAisVesselUnderCursor: ошибка при поиске судна", e);
clearAisVesselInfo();
}
}
/**
* Вычисляет расстояние между двумя точками в метрах
* Вычисляет адаптивный радиус поиска в пикселях в зависимости от масштаба карты
*/
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
final int R = 6371000; // Радиус Земли в метрах
private float calculateAdaptivePixelRadius(double zoom) {
// Базовый радиус 80 пикселей для среднего масштаба (zoom ~12)
// При увеличении масштаба радиус увеличивается, при уменьшении - уменьшается
float baseRadius = 80f;
float zoomFactor = (float) Math.pow(1.2, zoom - 12);
float radius = baseRadius * zoomFactor;
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;
// Ограничиваем радиус разумными пределами
return Math.max(40f, Math.min(200f, radius));
}
/**
* Вычисляет адаптивный радиус поиска в метрах в зависимости от масштаба карты
*/
private double calculateAdaptiveGeoRadius(double zoom) {
// Базовый радиус 200 метров для среднего масштаба (zoom ~12)
// При увеличении масштаба радиус уменьшается, при уменьшении - увеличивается
double baseRadius = 200.0;
double zoomFactor = Math.pow(0.7, zoom - 12);
double radius = baseRadius * zoomFactor;
// Ограничиваем радиус разумными пределами (от 50 метров до 2 км)
return Math.max(50.0, Math.min(2000.0, radius));
}
@Override
public void setAisVesselInfo(com.grigowashere.aismap.models.AISVessel vessel) {
Log.d(TAG, String.format("setAisVesselInfo: устанавливаем информацию о судне %s",
vessel != null ? vessel.getMmsi() : "null"));
if (cursorOverlay != null) {
cursorOverlay.setAisVesselInfo(vessel);
} else {
Log.d(TAG, "setAisVesselInfo: cursorOverlay равен null");
}
}
@Override
public void clearAisVesselInfo() {
Log.d(TAG, "clearAisVesselInfo: очищаем информацию о судне");
if (cursorOverlay != null) {
cursorOverlay.clearAisVesselInfo();
} else {
Log.d(TAG, "clearAisVesselInfo: cursorOverlay равен null");
}
}
/**
* Принудительно проверяет AIS судно под курсором (для восстановления панели после возврата в активность)
*/
public void forceCheckAisVesselUnderCursor() {
if (maplibreMap != null) {
try {
org.maplibre.android.geometry.LatLng center = maplibreMap.getCameraPosition().target;
Log.d(TAG, String.format("forceCheckAisVesselUnderCursor: принудительная проверка центра=%.6f,%.6f",
center.getLatitude(), center.getLongitude()));
checkAisVesselUnderCursor(center);
} catch (Exception e) {
Log.w(TAG, "forceCheckAisVesselUnderCursor: MapView may be destroyed: " + e.getMessage());
}
} else {
Log.d(TAG, "forceCheckAisVesselUnderCursor: maplibreMap равен null");
}
}
/**
* Обновляет размеры экрана для курсора (например, при повороте устройства)
*/
public void updateScreenDimensions() {
if (cursorOverlay != null && mapView != null) {
mapView.post(() -> {
int width = mapView.getWidth();
int height = mapView.getHeight();
if (width > 0 && height > 0) {
cursorOverlay.setScreenDimensions(width, height);
Log.d(TAG, String.format("Обновлены размеры экрана для курсора: %dx%d", width, height));
}
});
}
}
/**
* Обновляет отладочную визуализацию области поиска
*/
private void updateDebugVisualization() {
if (!debugMode || lastSearchRect == null || maplibreMap == null || style == null) {
return;
}
// Проверяем, что стиль готов к работе
if (!isStyleValid()) {
Log.d(TAG, "updateDebugVisualization: стиль не готов, пропускаем визуализацию");
return;
}
try {
// Удаляем предыдущий отладочный слой если есть
if (style.getLayer("debug-search-area") != null) {
style.removeLayer("debug-search-area");
}
if (style.getSource("debug-search-area") != null) {
style.removeSource("debug-search-area");
}
// Конвертируем экранные координаты обратно в географические
org.maplibre.android.geometry.LatLng topLeft = maplibreMap.getProjection()
.fromScreenLocation(new android.graphics.PointF(lastSearchRect.left, lastSearchRect.top));
org.maplibre.android.geometry.LatLng topRight = maplibreMap.getProjection()
.fromScreenLocation(new android.graphics.PointF(lastSearchRect.right, lastSearchRect.top));
org.maplibre.android.geometry.LatLng bottomRight = maplibreMap.getProjection()
.fromScreenLocation(new android.graphics.PointF(lastSearchRect.right, lastSearchRect.bottom));
org.maplibre.android.geometry.LatLng bottomLeft = maplibreMap.getProjection()
.fromScreenLocation(new android.graphics.PointF(lastSearchRect.left, lastSearchRect.bottom));
// Создаем полигон для отладочной области
java.util.List<org.maplibre.geojson.Point> coordinates = new java.util.ArrayList<>();
coordinates.add(org.maplibre.geojson.Point.fromLngLat(topLeft.getLongitude(), topLeft.getLatitude()));
coordinates.add(org.maplibre.geojson.Point.fromLngLat(topRight.getLongitude(), topRight.getLatitude()));
coordinates.add(org.maplibre.geojson.Point.fromLngLat(bottomRight.getLongitude(), bottomRight.getLatitude()));
coordinates.add(org.maplibre.geojson.Point.fromLngLat(bottomLeft.getLongitude(), bottomLeft.getLatitude()));
coordinates.add(org.maplibre.geojson.Point.fromLngLat(topLeft.getLongitude(), topLeft.getLatitude())); // Замыкаем полигон
java.util.List<java.util.List<org.maplibre.geojson.Point>> polygon = new java.util.ArrayList<>();
polygon.add(coordinates);
org.maplibre.geojson.Polygon debugPolygon = org.maplibre.geojson.Polygon.fromLngLats(polygon);
org.maplibre.geojson.Feature debugFeature = org.maplibre.geojson.Feature.fromGeometry(debugPolygon);
// Создаем источник данных
org.maplibre.android.style.sources.GeoJsonSource debugSource =
new org.maplibre.android.style.sources.GeoJsonSource("debug-search-area", debugFeature);
style.addSource(debugSource);
// Создаем слой для отображения
org.maplibre.android.style.layers.LineLayer debugLayer =
new org.maplibre.android.style.layers.LineLayer("debug-search-area", "debug-search-area");
debugLayer.setProperties(
org.maplibre.android.style.layers.PropertyFactory.lineColor(android.graphics.Color.RED),
org.maplibre.android.style.layers.PropertyFactory.lineWidth(2f),
org.maplibre.android.style.layers.PropertyFactory.lineOpacity(0.8f)
);
style.addLayer(debugLayer);
Log.d(TAG, "updateDebugVisualization: отладочный квадрат добавлен на карту");
} catch (Exception e) {
Log.e(TAG, "updateDebugVisualization: ошибка при добавлении отладочной визуализации", e);
}
}
/**
* Включает/выключает режим отладки
*/
public void setDebugMode(boolean enabled) {
this.debugMode = enabled;
if (!enabled && style != null && isStyleValid()) {
// Удаляем отладочную визуализацию
try {
if (style.getLayer("debug-search-area") != null) {
style.removeLayer("debug-search-area");
}
if (style.getSource("debug-search-area") != null) {
style.removeSource("debug-search-area");
}
Log.d(TAG, "setDebugMode: отладочная визуализация удалена");
} catch (Exception e) {
Log.e(TAG, "setDebugMode: ошибка при удалении отладочной визуализации", e);
}
}
}
@@ -2,6 +2,7 @@ package com.grigowashere.aismap.maps;
import android.graphics.Color;
import android.util.Log;
import com.grigowashere.aismap.utils.GeoUtils;
import com.yandex.mapkit.geometry.Point;
import com.yandex.mapkit.map.MapObjectCollection;
import com.yandex.mapkit.map.PolylineMapObject;
@@ -273,19 +274,10 @@ public class VesselPathTracker {
* Рассчитывает расстояние между двумя точками в метрах
*/
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; // радиус Земли в метрах
return GeoUtils.calculateDistance(
point1.getLatitude(), point1.getLongitude(),
point2.getLatitude(), point2.getLongitude()
);
}
/**
@@ -176,6 +176,25 @@ public class YandexMapImpl implements MapInterface {
return mapView.getMap().getCameraPosition().getZoom();
}
@Override
public void setBearing(float bearing) {
try {
CameraPosition current = mapView.getMap().getCameraPosition();
Point target = current.getTarget();
CameraPosition newPos = new CameraPosition(target, current.getZoom(), bearing, current.getTilt());
mapView.getMap().move(newPos, new Animation(Animation.Type.SMOOTH, 0.5f), null);
} catch (Exception ignore) {}
}
@Override
public float getBearing() {
try {
return mapView.getMap().getCameraPosition().getAzimuth();
} catch (Exception e) {
return 0f;
}
}
@Override
public void addLayer(String layerId, Object layerData) {
// Реализация добавления дополнительных слоев
@@ -79,6 +79,98 @@ public class GeoUtils {
);
}
/**
* Вычисляет относительный пеленг (сколько градусов влево/вправо от нашего курса)
* @param ourCourse наш курс в градусах (0-360)
* @param targetBearing пеленг до цели в градусах (0-360)
* @return относительный пеленг в градусах (-180 до +180, отрицательное = влево, положительное = вправо)
*/
public static double calculateRelativeBearing(double ourCourse, double targetBearing) {
if (ourCourse < 0 || targetBearing < 0) return -1;
double relativeBearing = targetBearing - ourCourse;
// Нормализуем к диапазону -180 до +180
while (relativeBearing > 180) relativeBearing -= 360;
while (relativeBearing < -180) relativeBearing += 360;
return relativeBearing;
}
/**
* Проверяет валидность координат
* @param latitude широта в градусах
* @param longitude долгота в градусах
* @return true если координаты валидны
*/
public static boolean isValidCoordinates(double latitude, double longitude) {
// Проверяем на нулевые координаты
if (latitude == 0.0 && longitude == 0.0) {
return false;
}
// Проверяем на невалидные координаты AIS (181, 91)
if (latitude == 91.0 && longitude == 181.0) {
return false;
}
// Проверяем на стандартные границы координат
if (latitude < -90.0 || latitude > 90.0) {
return false;
}
if (longitude < -180.0 || longitude > 180.0) {
return false;
}
return true;
}
/**
* Форматирует расстояние для отображения
* @param distanceMeters расстояние в метрах
* @return отформатированная строка
*/
public static String formatDistance(double distanceMeters) {
if (distanceMeters < 0) return "--";
if (distanceMeters < 1000) {
return String.format("%.0f м", distanceMeters);
} else {
return String.format("%.1f км", distanceMeters / 1000.0);
}
}
/**
* Форматирует относительный пеленг для отображения
* @param relativeBearing относительный пеленг в градусах
* @return отформатированная строка
*/
public static String formatRelativeBearing(double relativeBearing) {
// Проверяем на невалидные значения
if (relativeBearing < -180 || relativeBearing > 180) return "--";
if (Math.abs(relativeBearing) < 1) {
return "прямо";
} else if (relativeBearing > 0) {
return String.format("%.0f° вправо", relativeBearing);
} else {
return String.format("%.0f° влево", Math.abs(relativeBearing));
}
}
/**
* Нормализует угол к диапазону 0-360 градусов
* @param angle угол в градусах
* @return нормализованный угол (0-360)
*/
public static double normalizeAngle(double angle) {
if (Double.isNaN(angle) || Double.isInfinite(angle)) return 0.0;
double normalized = angle % 360.0;
if (normalized < 0) normalized += 360.0;
return normalized;
}
/**
* Конвертирует навигационный статус в числовой код
* @param navigationalStatus строковый статус
@@ -2,12 +2,10 @@ package com.grigowashere.aismap.utils;
/**
* Утилиты для навигационных вычислений
* Теперь использует GeoUtils для базовых геодезических расчетов
*/
public class NavigationUtils {
// Радиус Земли в метрах
private static final double EARTH_RADIUS_METERS = 6371000.0;
/**
* Вычисляет расстояние между двумя точками на Земле (формула гаверсинуса)
* @param lat1 широта первой точки в градусах
@@ -18,26 +16,11 @@ public class NavigationUtils {
*/
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
// Проверяем валидность координат
if (lat1 == 0 && lon1 == 0) return -1;
if (lat2 == 0 && lon2 == 0) return -1;
if (!GeoUtils.isValidCoordinates(lat1, lon1) || !GeoUtils.isValidCoordinates(lat2, lon2)) {
return -1;
}
// Преобразуем градусы в радианы
double lat1Rad = Math.toRadians(lat1);
double lon1Rad = Math.toRadians(lon1);
double lat2Rad = Math.toRadians(lat2);
double lon2Rad = Math.toRadians(lon2);
// Разности координат
double deltaLat = lat2Rad - lat1Rad;
double deltaLon = lon2Rad - lon1Rad;
// Формула гаверсинуса
double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_METERS * c;
return GeoUtils.calculateDistance(lat1, lon1, lat2, lon2);
}
/**
@@ -50,28 +33,11 @@ public class NavigationUtils {
*/
public static double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
// Проверяем валидность координат
if (lat1 == 0 && lon1 == 0) return -1;
if (lat2 == 0 && lon2 == 0) return -1;
if (!GeoUtils.isValidCoordinates(lat1, lon1) || !GeoUtils.isValidCoordinates(lat2, lon2)) {
return -1;
}
// Преобразуем градусы в радианы
double lat1Rad = Math.toRadians(lat1);
double lon1Rad = Math.toRadians(lon1);
double lat2Rad = Math.toRadians(lat2);
double lon2Rad = Math.toRadians(lon2);
// Разности координат
double deltaLon = lon2Rad - lon1Rad;
// Вычисляем азимут
double y = Math.sin(deltaLon) * Math.cos(lat2Rad);
double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLon);
double bearingRad = Math.atan2(y, x);
double bearingDeg = Math.toDegrees(bearingRad);
// Нормализуем к диапазону 0-360
return (bearingDeg + 360) % 360;
return GeoUtils.calculateBearing(lat1, lon1, lat2, lon2);
}
/**
@@ -81,15 +47,7 @@ public class NavigationUtils {
* @return относительный азимут в градусах (-180 до +180, отрицательное = влево, положительное = вправо)
*/
public static double calculateRelativeBearing(double ourCourse, double targetBearing) {
if (ourCourse < 0 || targetBearing < 0) return -1;
double relativeBearing = targetBearing - ourCourse;
// Нормализуем к диапазону -180 до +180
while (relativeBearing > 180) relativeBearing -= 360;
while (relativeBearing < -180) relativeBearing += 360;
return relativeBearing;
return GeoUtils.calculateRelativeBearing(ourCourse, targetBearing);
}
/**
@@ -98,13 +56,7 @@ public class NavigationUtils {
* @return отформатированная строка
*/
public static String formatDistance(double distanceMeters) {
if (distanceMeters < 0) return "--";
if (distanceMeters < 1000) {
return String.format("%.0f м", distanceMeters);
} else {
return String.format("%.1f км", distanceMeters / 1000.0);
}
return GeoUtils.formatDistance(distanceMeters);
}
/**
@@ -113,15 +65,6 @@ public class NavigationUtils {
* @return отформатированная строка
*/
public static String formatRelativeBearing(double relativeBearing) {
// Проверяем на невалидные значения
if (relativeBearing < -180 || relativeBearing > 180) return "--";
if (Math.abs(relativeBearing) < 1) {
return "прямо";
} else if (relativeBearing > 0) {
return String.format("%.0f° вправо", relativeBearing);
} else {
return String.format("%.0f° влево", Math.abs(relativeBearing));
}
return GeoUtils.formatRelativeBearing(relativeBearing);
}
}
@@ -3,12 +3,15 @@ package com.grigowashere.aismap.view;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.grigowashere.aismap.R;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.utils.GeoUtils;
/**
* Overlay для отображения курсора на карте с координатами и информацией о расстоянии
@@ -37,6 +40,20 @@ public class CursorOverlay {
private double cursorLatitude;
private double cursorLongitude;
// Размеры экрана для расчета позиций
private int screenWidth;
private int screenHeight;
private int centerX;
private int centerY;
// Отступы от центра
private static final int COORDINATES_OFFSET_X = -60;
private static final int COORDINATES_OFFSET_Y = -210;
private static final int DISTANCE_OFFSET_X = 60;
private static final int DISTANCE_OFFSET_Y = 60;
private static final int AIS_INFO_OFFSET_X = -60;
private static final int AIS_INFO_OFFSET_Y = 60;
public CursorOverlay(Context context) {
this.context = context;
initializeViews();
@@ -69,6 +86,194 @@ public class CursorOverlay {
return overlayView;
}
/**
* Устанавливает размеры экрана для расчета позиций панелей
*/
public void setScreenDimensions(int width, int height) {
this.screenWidth = width;
this.screenHeight = height;
this.centerX = width / 2;
this.centerY = height / 2;
// Обновляем позиции всех панелей
updatePanelPositions();
}
/**
* Обновляет позиции всех панелей относительно центра экрана
*/
private void updatePanelPositions() {
if (centerX == 0 || centerY == 0) {
return; // Размеры экрана еще не установлены
}
android.util.Log.d("CursorOverlay", "updatePanelPositions: обновляем позиции панелей");
// Позиционируем панель координат (верхний левый квадрант) - растет влево
positionPanel(coordinatesPanel, COORDINATES_OFFSET_X, COORDINATES_OFFSET_Y, false, true);
// Позиционируем панель расстояния и пеленга (нижний правый квадрант) - растет вправо
positionPanel(distanceBearingPanel, DISTANCE_OFFSET_X, DISTANCE_OFFSET_Y, true, true);
// НЕ позиционируем AIS панель здесь - она обновляется в updateAisVesselInfo
android.util.Log.d("CursorOverlay", "updatePanelPositions: пропускаем AIS панель, она обновляется отдельно");
}
/**
* Позиционирует панель относительно центра экрана
* @param panel панель для позиционирования
* @param offsetX смещение по X от центра
* @param offsetY смещение по Y от центра
* @param alignLeft выравнивать ли по левому краю (иначе по правому)
* @param alignTop выравнивать ли по верхнему краю (иначе по нижнему)
*/
private void positionPanel(View panel, int offsetX, int offsetY, boolean alignLeft, boolean alignTop) {
android.util.Log.d("CursorOverlay", String.format("positionPanel: panel=%s, centerX=%d, centerY=%d",
panel != null ? panel.getClass().getSimpleName() : "null", centerX, centerY));
if (panel == null || centerX == 0 || centerY == 0) {
android.util.Log.d("CursorOverlay", "positionPanel: выход - panel=null или размеры экрана не установлены");
return;
}
// Измеряем размеры панели
panel.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
int panelWidth = panel.getMeasuredWidth();
int panelHeight = panel.getMeasuredHeight();
android.util.Log.d("CursorOverlay", String.format("positionPanel: размеры панели width=%d, height=%d", panelWidth, panelHeight));
// Если панель еще не измерена, используем ViewTreeObserver
if (panelWidth == 0 || panelHeight == 0) {
android.util.Log.d("CursorOverlay", String.format("positionPanel: панель не измерена (width=%d, height=%d), ждем layout", panelWidth, panelHeight));
panel.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
panel.getViewTreeObserver().removeOnGlobalLayoutListener(this);
android.util.Log.d("CursorOverlay", "positionPanel: layout готов, повторяем позиционирование");
positionPanel(panel, offsetX, offsetY, alignLeft, alignTop);
}
});
return;
}
// Рассчитываем финальную позицию с учетом выравнивания
int finalX, finalY;
if (alignLeft) {
// Для левого выравнивания: левый край панели на offsetX от центра
finalX = centerX + offsetX;
} else {
// Для правого выравнивания: правый край панели на offsetX от центра
finalX = centerX + offsetX - panelWidth;
}
if (alignTop) {
// Для верхнего выравнивания: верхний край панели на offsetY от центра
finalY = centerY + offsetY;
} else {
// Для нижнего выравнивания: нижний край панели на offsetY от центра
finalY = centerY + offsetY - panelHeight;
}
android.util.Log.d("CursorOverlay", String.format("positionPanel: финальные координаты finalX=%d, finalY=%d, alignLeft=%b, alignTop=%b",
finalX, finalY, alignLeft, alignTop));
// Устанавливаем позицию через LayoutParams
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
);
params.leftMargin = finalX;
params.topMargin = finalY;
// Убираем все правила выравнивания, которые могут конфликтовать
params.addRule(RelativeLayout.ALIGN_PARENT_LEFT, 0);
params.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, 0);
params.addRule(RelativeLayout.ALIGN_PARENT_TOP, 0);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, 0);
params.addRule(RelativeLayout.CENTER_IN_PARENT, 0);
params.addRule(RelativeLayout.CENTER_HORIZONTAL, 0);
params.addRule(RelativeLayout.CENTER_VERTICAL, 0);
panel.setLayoutParams(params);
android.util.Log.d("CursorOverlay", "positionPanel: LayoutParams установлены");
// Альтернативный способ - используем setX/setY
panel.setX(finalX);
panel.setY(finalY);
android.util.Log.d("CursorOverlay", String.format("positionPanel: setX/setY установлены: x=%.1f, y=%.1f", panel.getX(), panel.getY()));
// Принудительно обновляем layout
panel.requestLayout();
android.util.Log.d("CursorOverlay", "positionPanel: requestLayout вызван");
// Принудительно обновляем layout родителя
if (panel.getParent() instanceof ViewGroup) {
((ViewGroup) panel.getParent()).requestLayout();
android.util.Log.d("CursorOverlay", "positionPanel: requestLayout родителя вызван");
}
// Принудительно поднимаем AIS панель наверх
if (panel == aisVesselInfoPanel) {
panel.bringToFront();
panel.setElevation(20f); // Принудительно поднимаем elevation выше курсора
android.util.Log.d("CursorOverlay", "positionPanel: AIS панель поднята наверх с elevation=20");
// Принудительно поднимаем панель над курсором
View cursorCross = overlayView.findViewById(R.id.cursor_cross);
if (cursorCross != null) {
cursorCross.setElevation(5f); // Курсор ниже AIS панели
android.util.Log.d("CursorOverlay", "positionPanel: курсор опущен с elevation=5");
}
// Проверяем видимость панели
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: видимость=%d, alpha=%.2f, x=%.1f, y=%.1f",
panel.getVisibility(), panel.getAlpha(), panel.getX(), panel.getY()));
// Проверяем позицию после всех операций
panel.post(() -> {
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: финальная позиция x=%.1f, y=%.1f, width=%d, height=%d",
panel.getX(), panel.getY(), panel.getWidth(), panel.getHeight()));
// Проверяем, не выходит ли панель за границы экрана
float right = panel.getX() + panel.getWidth();
float bottom = panel.getY() + panel.getHeight();
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: границы панели left=%.1f, top=%.1f, right=%.1f, bottom=%.1f",
panel.getX(), panel.getY(), right, bottom));
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: размеры экрана width=%d, height=%d",
screenWidth, screenHeight));
// Проверяем, выходит ли панель за границы экрана
boolean outOfBounds = panel.getX() < 0 || panel.getY() < 0 || right > screenWidth || bottom > screenHeight;
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: панель за границами экрана=%b", outOfBounds));
if (outOfBounds) {
android.util.Log.w("CursorOverlay", String.format("positionPanel AIS: ПРОБЛЕМА! Панель выходит за границы экрана!"));
android.util.Log.w("CursorOverlay", String.format("positionPanel AIS: left=%.1f < 0? %b", panel.getX(), panel.getX() < 0));
android.util.Log.w("CursorOverlay", String.format("positionPanel AIS: top=%.1f < 0? %b", panel.getY(), panel.getY() < 0));
android.util.Log.w("CursorOverlay", String.format("positionPanel AIS: right=%.1f > %d? %b", right, screenWidth, right > screenWidth));
android.util.Log.w("CursorOverlay", String.format("positionPanel AIS: bottom=%.1f > %d? %b", bottom, screenHeight, bottom > screenHeight));
}
// Проверяем видимость панели
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: видимость=%d, alpha=%.2f, elevation=%.1f",
panel.getVisibility(), panel.getAlpha(), panel.getElevation()));
// Проверяем фон панели
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: фон панели=%s",
panel.getBackground() != null ? panel.getBackground().getClass().getSimpleName() : "null"));
});
}
// Отладочная информация для AIS панели
if (panel == aisVesselInfoPanel) {
android.util.Log.d("CursorOverlay", String.format("positionPanel AIS: centerX=%d, centerY=%d, offsetX=%d, offsetY=%d, panelWidth=%d, panelHeight=%d, finalX=%d, finalY=%d, alignLeft=%b, alignTop=%b",
centerX, centerY, offsetX, offsetY, panelWidth, panelHeight, finalX, finalY, alignLeft, alignTop));
}
}
/**
* Обновляет координаты курсора (центра экрана)
*/
@@ -79,6 +284,9 @@ public class CursorOverlay {
tvCursorLatitude.setText(String.format("%.6f°", latitude));
tvCursorLongitude.setText(String.format("%.6f°", longitude));
// Обновляем позицию панели координат после изменения содержимого
coordinatesPanel.post(() -> positionPanel(coordinatesPanel, COORDINATES_OFFSET_X, COORDINATES_OFFSET_Y, false, true));
// Обновляем информацию о расстоянии и пеленге, если есть данные о нашем судне
updateDistanceAndBearing();
}
@@ -96,37 +304,28 @@ public class CursorOverlay {
*/
private void updateDistanceAndBearing() {
if (ownVessel != null && isValidPosition(ownVessel)) {
double distance = calculateDistance(
double distance = GeoUtils.calculateDistance(
ownVessel.getLatitude(), ownVessel.getLongitude(),
cursorLatitude, cursorLongitude
);
// Вычисляем пеленг от судна к курсору
double bearingToCursor = calculateBearing(
double bearingToCursor = GeoUtils.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;
relativeBearing = GeoUtils.calculateRelativeBearing(ownVessel.getCourse(), bearingToCursor);
} else {
// Если курс неизвестен, показываем абсолютный пеленг
relativeBearing = bearingToCursor;
}
// Форматируем расстояние: в км с дробной частью если > 1000м, иначе в метрах
String distanceText;
if (distance >= 1000) {
distanceText = String.format("Rng: %.2f км", distance / 1000.0);
} else {
distanceText = String.format("Rng: %.1f м", distance);
}
// Форматируем расстояние
String distanceText = "Rng: " + GeoUtils.formatDistance(distance);
tvDistance.setText(distanceText);
tvBearing.setText(String.format("Brg: %.1f°", relativeBearing));
@@ -134,6 +333,9 @@ public class CursorOverlay {
// Показываем информацию о расстоянии и пеленге
tvDistance.setVisibility(View.VISIBLE);
tvBearing.setVisibility(View.VISIBLE);
// Обновляем позицию панели после изменения содержимого
distanceBearingPanel.post(() -> positionPanel(distanceBearingPanel, DISTANCE_OFFSET_X, DISTANCE_OFFSET_Y, true, true));
} else {
// Скрываем информацию, если нет валидных координат нашего судна
tvDistance.setVisibility(View.GONE);
@@ -141,40 +343,6 @@ public class CursorOverlay {
}
}
/**
* Вычисляет расстояние между двумя точками в метрах (формула гаверсинуса)
*/
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
}
/**
* Скрывает курсор
@@ -198,6 +366,8 @@ public class CursorOverlay {
* Устанавливает информацию об AIS судне под курсором
*/
public void setAisVesselInfo(AISVessel vessel) {
android.util.Log.d("CursorOverlay", String.format("setAisVesselInfo: получили судно %s",
vessel != null ? vessel.getMmsi() : "null"));
this.currentAisVessel = vessel;
updateAisVesselInfo();
}
@@ -205,7 +375,10 @@ public class CursorOverlay {
/**
* Обновляет отображение информации об AIS судне
*/
///TO DO у нас тут ошибка после смены активности, надо ее исправить
private void updateAisVesselInfo() {
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: обновляем информацию, судно=%s",
currentAisVessel != null ? currentAisVessel.getMmsi() : "null"));
if (currentAisVessel != null) {
// MMSI
tvAisMmsi.setText("MMSI: " + currentAisVessel.getMmsi());
@@ -244,11 +417,57 @@ public class CursorOverlay {
tvAisSog.setVisibility(View.GONE);
}
// Показываем панель
// Показываем панель СНАЧАЛА
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: ДО установки видимости - видимость=%d, alpha=%.2f",
aisVesselInfoPanel.getVisibility(), aisVesselInfoPanel.getAlpha()));
aisVesselInfoPanel.setVisibility(View.VISIBLE);
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: ПОСЛЕ установки видимости - видимость=%d, alpha=%.2f",
aisVesselInfoPanel.getVisibility(), aisVesselInfoPanel.getAlpha()));
// Обновляем позицию после изменения содержимого
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: родитель панели=%s",
aisVesselInfoPanel.getParent() != null ? aisVesselInfoPanel.getParent().getClass().getSimpleName() : "null"));
// Вызываем позиционирование
android.util.Log.d("CursorOverlay", "updateAisVesselInfo: вызываем positionPanel");
positionPanel(aisVesselInfoPanel, AIS_INFO_OFFSET_X, AIS_INFO_OFFSET_Y, false, true);
// Проверяем видимость после позиционирования
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: после позиционирования - видимость=%d, alpha=%.2f, x=%.1f, y=%.1f",
aisVesselInfoPanel.getVisibility(), aisVesselInfoPanel.getAlpha(),
aisVesselInfoPanel.getX(), aisVesselInfoPanel.getY()));
// Проверяем содержимое панели
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: содержимое панели - MMSI=%s, Name=%s, CallSign=%s, COG=%.1f, SOG=%.1f",
currentAisVessel.getMmsi(),
currentAisVessel.getVesselName() != null ? currentAisVessel.getVesselName() : "null",
currentAisVessel.getCallSign() != null ? currentAisVessel.getCallSign() : "null",
currentAisVessel.getCourse(),
currentAisVessel.getSpeed()));
// Проверяем видимость элементов панели
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: видимость элементов - MMSI=%d, Name=%d, CallSign=%d, COG=%d, SOG=%d",
tvAisMmsi.getVisibility(),
tvAisName.getVisibility(),
tvAisCallSign.getVisibility(),
tvAisCog.getVisibility(),
tvAisSog.getVisibility()));
// Проверяем видимость родительских контейнеров
if (overlayView != null) {
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: overlayView видимость=%d, alpha=%.2f",
overlayView.getVisibility(), overlayView.getAlpha()));
}
if (aisVesselInfoPanel.getParent() != null) {
android.util.Log.d("CursorOverlay", String.format("updateAisVesselInfo: родитель панели видимость=%d, alpha=%.2f",
((View) aisVesselInfoPanel.getParent()).getVisibility(), ((View) aisVesselInfoPanel.getParent()).getAlpha()));
}
} else {
// Скрываем панель
aisVesselInfoPanel.setVisibility(View.GONE);
android.util.Log.d("CursorOverlay", "updateAisVesselInfo: скрываем панель");
}
}
@@ -256,6 +475,7 @@ public class CursorOverlay {
* Очищает информацию об AIS судне
*/
public void clearAisVesselInfo() {
android.util.Log.d("CursorOverlay", "clearAisVesselInfo: очищаем информацию о судне");
this.currentAisVessel = null;
aisVesselInfoPanel.setVisibility(View.GONE);
}
@@ -266,11 +486,6 @@ public class CursorOverlay {
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; // Исключаем нулевые координаты
return GeoUtils.isValidCoordinates(vessel.getLatitude(), vessel.getLongitude());
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="#80000000" />
<size
android:width="40dp"
android:height="40dp" />
</shape>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="236dp"
android:height="236dp"
android:viewportWidth="236"
android:viewportHeight="236">
<path
android:pathData="M226.29,89.5h-19.67c-7.31,0 -10.97,-8.84 -5.8,-14.01l13.91,-13.91c3.21,-3.21 3.21,-8.4 0,-11.61l-28.7,-28.7c-3.21,-3.21 -8.4,-3.21 -11.61,0l-13.91,13.91c-5.17,5.17 -14.01,1.51 -14.01,-5.8V9.71c0,-4.53 -3.67,-8.21 -8.21,-8.21h-40.58c-4.53,0 -8.21,3.67 -8.21,8.21v19.67c0,7.31 -8.84,10.97 -14.01,5.8l-13.91,-13.91c-3.21,-3.21 -8.4,-3.21 -11.61,0l-28.7,28.7c-3.21,3.21 -3.21,8.4 0,11.61l13.91,13.91c5.17,5.17 1.51,14.01 -5.8,14.01H9.71c-4.53,0 -8.21,3.67 -8.21,8.21v40.58c0,4.53 3.67,8.21 8.21,8.21h19.67c7.31,0 10.97,8.84 5.8,14.01l-13.91,13.91c-3.21,3.21 -3.21,8.4 0,11.61l28.7,28.7c3.21,3.21 8.4,3.21 11.61,0l13.91,-13.91c5.17,-5.17 14.01,-1.51 14.01,5.8v19.67c0,4.53 3.67,8.21 8.21,8.21h40.58c4.53,0 8.21,-3.67 8.21,-8.21v-19.67c0,-7.31 8.84,-10.97 14.01,-5.8l13.91,13.91c3.21,3.21 8.4,3.21 11.61,0l28.7,-28.7c3.21,-3.21 3.21,-8.4 0,-11.61l-13.91,-13.91c-5.17,-5.17 -1.51,-14.01 5.8,-14.01h19.67c4.53,0 8.21,-3.67 8.21,-8.21v-40.58c0,-4.53 -3.67,-8.21 -8.21,-8.21ZM118.5,158.5c-22.09,0 -40,-17.91 -40,-40s17.91,-40 40,-40 40,17.91 40,40 -17.91,40 -40,40Z"
android:strokeWidth="3"
android:fillColor="#666"
android:strokeColor="#000"/>
</vector>
+16
View File
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="66.46dp"
android:height="176.77dp"
android:viewportWidth="66.46"
android:viewportHeight="176.77">
<path
android:strokeWidth="1"
android:pathData="M33.73,64.39c12.24,0 22.33,9.16 23.81,21h8.19L33.23,1.39 0.73,85.39h9.19c1.48,-11.84 11.57,-21 23.81,-21Z"
android:fillColor="#e32636"
android:strokeColor="#961923"/>
<path
android:strokeWidth="1"
android:pathData="M57.04,91.39c-1.48,11.84 -11.57,21 -23.81,21s-22.33,-9.16 -23.81,-21H0.73l32.5,84 32.5,-84h-8.69Z"
android:fillColor="#c0c0c0"
android:strokeColor="#f3f3f3"/>
</vector>
+11
View File
@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="92.09dp"
android:height="199.81dp"
android:viewportWidth="92.09"
android:viewportHeight="199.81">
<path
android:pathData="M44.03,168.47c15.02,0 27.22,12.04 27.49,27l15.45,2.61 3.19,-23.41c0.89,-6.53 0.41,-13.17 -1.39,-19.5L46.03,5.47 3.33,155.13c-1.81,6.36 -2.28,13.02 -1.38,19.57l3.2,23.26 11.4,-2.5c0.27,-14.96 12.47,-27 27.49,-27Z"
android:strokeWidth="3"
android:fillColor="#d2ff1a"
android:strokeColor="#a8cc14"/>
</vector>
+69
View File
@@ -0,0 +1,69 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="233.17dp"
android:height="233.87dp"
android:viewportWidth="233.17"
android:viewportHeight="233.87">
<group>
<clip-path
android:pathData="M0,0h233.17v233.87h-233.17z"/>
<path
android:fillColor="#FF000000"
android:strokeWidth="1"
android:pathData="M26.7,0.5L207.3,0.5A26.2,26.2 0,0 1,233.5 26.7L233.5,27.3A26.2,26.2 0,0 1,207.3 53.5L26.7,53.5A26.2,26.2 0,0 1,0.5 27.3L0.5,26.7A26.2,26.2 0,0 1,26.7 0.5z"
android:strokeColor="#000"/>
<path
android:fillColor="#FF000000"
android:strokeWidth="1"
android:pathData="M26.7,60.39L207.3,60.39A26.2,26.2 0,0 1,233.5 86.59L233.5,87.19A26.2,26.2 0,0 1,207.3 113.39L26.7,113.39A26.2,26.2 0,0 1,0.5 87.19L0.5,86.59A26.2,26.2 0,0 1,26.7 60.39z"
android:strokeColor="#000"/>
<path
android:fillColor="#FF000000"
android:strokeWidth="1"
android:pathData="M26.7,120.28L207.3,120.28A26.2,26.2 0,0 1,233.5 146.48L233.5,147.08A26.2,26.2 0,0 1,207.3 173.28L26.7,173.28A26.2,26.2 0,0 1,0.5 147.08L0.5,146.48A26.2,26.2 0,0 1,26.7 120.28z"
android:strokeColor="#000"/>
<path
android:fillColor="#FF000000"
android:strokeWidth="1"
android:pathData="M26.7,180.17L207.3,180.17A26.2,26.2 0,0 1,233.5 206.37L233.5,206.97A26.2,26.2 0,0 1,207.3 233.17L26.7,233.17A26.2,26.2 0,0 1,0.5 206.97L0.5,206.37A26.2,26.2 0,0 1,26.7 180.17z"
android:strokeColor="#000"/>
</group>
<path
android:pathData="M89.21,29.73h0.09l4.98,-15.68h6.77v22.41h-5.12v-14.44l-0.09,-0.02 -4.86,14.45h-3.44l-4.77,-14.24 -0.09,0.02v14.22h-5.12V14.05h6.71l4.93,15.68Z"
android:fillColor="#fff"/>
<path
android:pathData="M116.2,29.73h0.09l4.98,-15.68h6.77v22.41h-5.12v-14.44l-0.09,-0.02 -4.86,14.45h-3.44l-4.77,-14.24 -0.09,0.02v14.22h-5.12V14.05h6.71l4.93,15.68Z"
android:fillColor="#fff"/>
<path
android:pathData="M142.92,30.52c0,-0.8 -0.26,-1.41 -0.77,-1.84s-1.45,-0.88 -2.8,-1.36c-2.73,-0.9 -4.77,-1.86 -6.09,-2.89 -1.33,-1.02 -1.99,-2.49 -1.99,-4.41s0.77,-3.4 2.32,-4.56c1.54,-1.16 3.51,-1.74 5.89,-1.74 2.51,0 4.54,0.6 6.07,1.79 1.53,1.2 2.28,2.88 2.23,5.06l-0.03,0.09h-4.96c0,-1.06 -0.28,-1.82 -0.85,-2.3 -0.57,-0.48 -1.42,-0.72 -2.56,-0.72 -0.93,0 -1.66,0.23 -2.19,0.69 -0.54,0.46 -0.8,1.03 -0.8,1.71s0.27,1.18 0.83,1.58c0.55,0.4 1.58,0.89 3.08,1.48 2.55,0.77 4.49,1.71 5.8,2.82 1.31,1.11 1.97,2.63 1.97,4.56s-0.76,3.51 -2.27,4.62 -3.52,1.67 -6.02,1.67 -4.6,-0.6 -6.33,-1.79c-1.73,-1.2 -2.57,-3.08 -2.52,-5.64l0.03,-0.09h4.98c0,1.3 0.32,2.23 0.95,2.78 0.63,0.55 1.6,0.82 2.9,0.82 1.07,0 1.87,-0.22 2.39,-0.65 0.52,-0.43 0.79,-1 0.79,-1.69Z"
android:fillColor="#fff"/>
<path
android:pathData="M156.24,36.46h-5.1V14.05h5.1v22.41Z"
android:fillColor="#fff"/>
<path
android:pathData="M104.15,87.67l0.03,0.09c0.04,2.51 -0.67,4.42 -2.13,5.71 -1.46,1.3 -3.52,1.94 -6.2,1.94s-4.91,-0.83 -6.56,-2.5c-1.65,-1.67 -2.48,-3.84 -2.48,-6.54v-4.6c0,-2.68 0.79,-4.86 2.38,-6.53 1.59,-1.67 3.69,-2.51 6.3,-2.51 2.79,0 4.96,0.65 6.49,1.95 1.53,1.3 2.27,3.19 2.23,5.68l-0.05,0.09h-4.98c0,-1.35 -0.29,-2.32 -0.86,-2.91 -0.58,-0.58 -1.52,-0.88 -2.83,-0.88 -1.15,0 -2.03,0.46 -2.65,1.39 -0.62,0.92 -0.92,2.15 -0.92,3.69v4.63c0,1.54 0.34,2.78 1.01,3.71 0.68,0.93 1.64,1.39 2.91,1.39 1.17,0 2.02,-0.29 2.54,-0.88 0.52,-0.58 0.78,-1.56 0.78,-2.94h4.98Z"
android:fillColor="#fff"/>
<path
android:pathData="M125.32,86.07c0,2.71 -0.86,4.95 -2.58,6.71 -1.72,1.76 -3.97,2.64 -6.74,2.64s-5.06,-0.88 -6.8,-2.64c-1.74,-1.76 -2.6,-4 -2.6,-6.71v-3.97c0,-2.7 0.87,-4.94 2.6,-6.71 1.73,-1.77 3.99,-2.65 6.77,-2.65s5.02,0.88 6.75,2.65c1.74,1.77 2.6,4 2.6,6.71v3.97ZM120.22,82.07c0,-1.57 -0.37,-2.87 -1.11,-3.88 -0.74,-1.01 -1.79,-1.51 -3.14,-1.51s-2.44,0.5 -3.17,1.51c-0.73,1 -1.1,2.3 -1.1,3.88v4c0,1.59 0.37,2.9 1.11,3.91 0.74,1.01 1.8,1.51 3.19,1.51s2.38,-0.5 3.12,-1.51c0.74,-1.01 1.1,-2.31 1.1,-3.91v-4Z"
android:fillColor="#fff"/>
<path
android:pathData="M145.85,92.06c-0.77,0.93 -1.85,1.72 -3.24,2.38 -1.39,0.66 -3.18,0.98 -5.37,0.98 -2.73,0 -4.96,-0.84 -6.66,-2.51 -1.71,-1.67 -2.56,-3.85 -2.56,-6.52v-4.6c0,-2.65 0.83,-4.82 2.49,-6.51 1.66,-1.69 3.8,-2.53 6.41,-2.53 2.82,0 4.94,0.63 6.38,1.88 1.44,1.26 2.13,2.97 2.08,5.15l-0.03,0.09h-4.8c0,-1.08 -0.29,-1.88 -0.86,-2.41 -0.58,-0.52 -1.44,-0.79 -2.6,-0.79s-2.15,0.47 -2.88,1.41 -1.09,2.16 -1.09,3.66v4.63c0,1.53 0.36,2.77 1.08,3.7 0.72,0.93 1.73,1.4 3.03,1.4 0.94,0 1.68,-0.08 2.22,-0.23 0.54,-0.15 0.97,-0.35 1.28,-0.61v-3.94h-3.91v-3.39h9.02v8.73Z"
android:fillColor="#fff"/>
<path
android:pathData="M98.85,151.26c0,-0.79 -0.26,-1.39 -0.77,-1.81 -0.51,-0.42 -1.45,-0.87 -2.8,-1.34 -2.73,-0.89 -4.77,-1.83 -6.09,-2.84 -1.33,-1 -1.99,-2.45 -1.99,-4.34s0.77,-3.34 2.32,-4.48c1.54,-1.14 3.51,-1.71 5.89,-1.71 2.51,0 4.54,0.59 6.07,1.76 1.53,1.18 2.28,2.83 2.23,4.97l-0.03,0.09h-4.96c0,-1.04 -0.28,-1.79 -0.85,-2.26 -0.57,-0.47 -1.42,-0.7 -2.56,-0.7 -0.93,0 -1.66,0.23 -2.19,0.68 -0.54,0.45 -0.8,1.01 -0.8,1.68s0.27,1.16 0.83,1.55c0.55,0.39 1.58,0.88 3.08,1.46 2.55,0.76 4.49,1.68 5.8,2.77 1.31,1.09 1.97,2.58 1.97,4.48s-0.76,3.45 -2.27,4.55c-1.51,1.09 -3.52,1.64 -6.02,1.64s-4.6,-0.59 -6.33,-1.76c-1.73,-1.18 -2.57,-3.02 -2.52,-5.55l0.03,-0.09h4.98c0,1.28 0.32,2.19 0.95,2.73 0.63,0.54 1.6,0.81 2.9,0.81 1.07,0 1.87,-0.21 2.39,-0.64 0.52,-0.42 0.79,-0.98 0.79,-1.67Z"
android:fillColor="#fff"/>
<path
android:pathData="M125.07,148.07c0,2.71 -0.86,4.95 -2.58,6.71 -1.72,1.76 -3.97,2.64 -6.74,2.64s-5.06,-0.88 -6.8,-2.64c-1.74,-1.76 -2.6,-4 -2.6,-6.71v-3.97c0,-2.7 0.87,-4.94 2.6,-6.71 1.73,-1.77 3.99,-2.65 6.77,-2.65s5.02,0.88 6.75,2.65c1.74,1.77 2.6,4 2.6,6.71v3.97ZM119.97,144.07c0,-1.57 -0.37,-2.87 -1.11,-3.88 -0.74,-1.01 -1.79,-1.51 -3.14,-1.51s-2.44,0.5 -3.17,1.51c-0.73,1 -1.1,2.3 -1.1,3.88v4c0,1.59 0.37,2.9 1.11,3.91 0.74,1.01 1.8,1.51 3.19,1.51s2.38,-0.5 3.12,-1.51c0.74,-1.01 1.1,-2.31 1.1,-3.91v-4Z"
android:fillColor="#fff"/>
<path
android:pathData="M145.59,154.06c-0.77,0.93 -1.85,1.72 -3.24,2.38 -1.39,0.66 -3.18,0.98 -5.37,0.98 -2.73,0 -4.96,-0.84 -6.66,-2.51 -1.71,-1.67 -2.56,-3.85 -2.56,-6.52v-4.6c0,-2.65 0.83,-4.82 2.49,-6.51 1.66,-1.69 3.8,-2.53 6.41,-2.53 2.82,0 4.94,0.63 6.38,1.88 1.44,1.26 2.13,2.97 2.08,5.15l-0.03,0.09h-4.8c0,-1.08 -0.29,-1.88 -0.86,-2.41 -0.58,-0.52 -1.44,-0.79 -2.6,-0.79s-2.15,0.47 -2.88,1.41 -1.09,2.16 -1.09,3.66v4.63c0,1.53 0.36,2.77 1.08,3.7 0.72,0.93 1.73,1.4 3.03,1.4 0.94,0 1.68,-0.08 2.22,-0.23 0.54,-0.15 0.97,-0.35 1.28,-0.61v-3.94h-3.91v-3.39h9.02v8.73Z"
android:fillColor="#fff"/>
<path
android:pathData="M104.08,208.1h-5.1v-4.3h5.1v4.3Z"
android:fillColor="#fff"/>
<path
android:pathData="M113.45,208.1h-5.1v-4.3h5.1v4.3Z"
android:fillColor="#fff"/>
<path
android:pathData="M122.82,208.1h-5.1v-4.3h5.1v4.3Z"
android:fillColor="#fff"/>
</vector>
+34 -29
View File
@@ -19,49 +19,54 @@
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_margin="16dp"
android:layout_marginEnd="8dp"
android:background="@android:color/transparent"
android:orientation="vertical"
android:padding="8dp"
android:gravity="end"
android:elevation="4dp">
<Button
<ImageButton
android:id="@+id/btn_center_vessel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Центр на судне"
android:textSize="12sp"
android:minWidth="120dp"
android:background="@android:color/white"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/button_background"
android:src="@drawable/ownship"
android:contentDescription="Центр на судне"
android:padding="8dp"
android:scaleType="fitCenter"
android:layout_marginBottom="8dp" />
<Button
<ImageButton
android:id="@+id/btn_map_orientation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Карта по северу"
android:textSize="12sp"
android:minWidth="120dp"
android:background="@android:color/white"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/button_background"
android:src="@drawable/compass"
android:contentDescription="Карта по северу"
android:padding="8dp"
android:scaleType="fitCenter"
android:layout_marginBottom="8dp" />
<Button
<ImageButton
android:id="@+id/btn_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚙️ Настройки"
android:textSize="12sp"
android:minWidth="120dp"
android:background="@android:color/white" />
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/button_background"
android:src="@drawable/cog"
android:contentDescription="Настройки"
android:padding="8dp"
android:scaleType="fitCenter" />
<Button
<ImageButton
android:id="@+id/btn_ais_targets"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Цели AIS"
android:textSize="12sp"
android:minWidth="120dp"
android:background="@android:color/white"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/button_background"
android:src="@drawable/targetlist"
android:contentDescription="Цели AIS"
android:padding="8dp"
android:scaleType="fitCenter"
android:layout_marginTop="8dp" />
+1 -15
View File
@@ -13,18 +13,13 @@
android:background="@drawable/cursorcross" />
<!-- Координаты в первом квадранте (верхний левый) -->
<!-- Расстояние и пеленг в четвертом квадранте (нижний правый) -->
<LinearLayout
android:id="@+id/coordinates_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/panel_background"
android:orientation="vertical"
android:padding="8dp"
android:translationX="-60dp"
android:translationY="-40dp"
android:visibility="visible">
<TextView
@@ -47,18 +42,14 @@
</LinearLayout>
<!-- Расстояние и пеленг в четвертом квадранте (нижний правый) -->
<LinearLayout
android:id="@+id/distance_bearing_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="120dp"
android:layout_marginTop="60dp"
android:background="@drawable/panel_background"
android:orientation="vertical"
android:padding="8dp"
android:translationX="60dp"
android:translationY="40dp"
android:visibility="visible">
<TextView
@@ -90,14 +81,9 @@
android:id="@+id/ais_vessel_info_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_marginStart="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/panel_background"
android:orientation="vertical"
android:padding="8dp"
android:translationX="-75dp"
android:translationY="60dp"
android:visibility="gone">
<TextView
+352
View File
@@ -0,0 +1,352 @@
# Диаграмма классов AISMap
```mermaid
classDiagram
%% ========== MAIN ACTIVITY ==========
class MainActivity {
-AppController appController
-MapController mapController
-MapInterface mapInterface
-UIRenderingCoordinator uiCoordinator
-CompassView compassView
-CoordinatesDockWidget coordinatesWidget
-SettingsManager settingsManager
+onCreate()
+onStart()
+onStop()
+checkPermissions()
}
%% ========== CORE CONTROLLERS ==========
class AppController {
-Context context
-NMEAParser nmeaParser
-UDPListener udpListener
-AndroidNMEAListener androidNmeaListener
-GPSLocationListener gpsLocationListener
-MapInterface mapInterface
-Repository repository
-Vessel ownVessel
-Map~String,AISVessel~ activeVessels
-ExecutorService executor
+setMapInterface(MapInterface)
+startGPS()
+startUDP()
+getNearbyVessels()
+cleanup()
+Note: "Runtime State Manager"
}
class MapController {
-MapInterface mapInterface
-VesselPathController pathController
+initialize()
+updateVesselPosition()
+addAISVessel()
}
class VesselPathController {
-List~VesselPathPoint~ pathPoints
-int maxPathPoints
+addPathPoint(Vessel)
+getPath()
+clearPath()
}
%% ========== DATA MODELS ==========
class Vessel {
-double latitude
-double longitude
-double course
-double speed
-double heading
-int signalStrength
-LocalDateTime lastUpdate
-String vesselName
-String mmsi
-double altitude
-int satellites
-double pdop, hdop, vdop
+updatePosition()
+updateGPSQuality()
+getGPSQualityPercentage()
}
class AISVessel {
-String mmsi
-String vesselName
-String callSign
-int imo
-String vesselType
-double latitude, longitude
-double course, speed
-double heading, rateOfTurn
-double length, width, draft
-String destination
-LocalDateTime eta
-String navigationalStatus
+updatePosition()
+isDataStale()
+shouldBeRemoved()
}
class VesselPathPoint {
-double latitude
-double longitude
-LocalDateTime timestamp
-double course
-double speed
}
%% ========== MAP INTERFACES ==========
class MapInterface {
<<interface>>
+initialize()
+cleanup()
+addOwnVesselMarker(Vessel)
+updateOwnVesselPosition(Vessel)
+addAISVesselMarker(AISVessel)
+updateAISVesselPosition(AISVessel)
+removeAISVesselMarker(String)
+centerOnPosition(double, double)
+setMarkerClickListener(MarkerClickListener)
}
class YandexMapImpl {
-MapView mapView
-Map~String, PlacemarkMapObject~ aisMarkers
-PlacemarkMapObject ownVesselMarker
+initialize()
+addOwnVesselMarker()
+updateOwnVesselPosition()
}
class MapLibreMapImpl {
-MapView mapView
-GeoJsonSource source
-Map~String, JSONObject~ idToFeature
-Handler uiHandler
+initialize()
+refreshGeoJson()
+updateOwnVesselPosition()
}
class MapForgeImpl {
-MapView mapView
-List~Marker~ aisMarkers
-Marker ownVesselMarker
+initialize()
+addMarker()
+updateMarker()
}
%% ========== DATA PARSERS ==========
class NMEAParser {
-Vessel ownVessel
-List~AISVessel~ aisVessels
-NMEAParserListener listener
-GPSLocationListener gpsLocationListener
-boolean hybridMode
+parseNMEA(String)
+setHybridMode(boolean)
+parseGGA()
+parseRMC()
+parseAIS()
}
class UDPListener {
-int port
-DatagramSocket socket
-UDPListenerCallback callback
-boolean isListening
+startListening()
+stopListening()
+sendData()
}
class AndroidNMEAListener {
-LocationManager locationManager
-NMEAMessageCallback callback
+startListening()
+stopListening()
}
class GPSLocationListener {
-LocationManager locationManager
-LocationCallback callback
-Location lastLocation
+startLocationUpdates()
+stopLocationUpdates()
}
%% ========== UI COMPONENTS ==========
class CompassView {
-float targetAzimuth
-float currentAzimuth
-float magneticCompass
-List~AISVessel~ nearbyVessels
-Vessel ourVessel
+updateAzimuth(float)
+updateNearbyVessels()
+onDrawDock()
}
class CoordinatesDockWidget {
-Vessel vessel
-String coordinatesText
-String sogText
-String cogText
+updateVessel(Vessel)
+onDrawDock()
}
class BaseDockWidget {
<<abstract>>
-boolean isDockTop
-OnDockResizeListener resizeListener
+setDockPosition()
#onDrawDock()
#dp(int)
}
class CursorOverlay {
-TextView tvCursorLatitude
-TextView tvCursorLongitude
-TextView tvDistance
-Vessel ownVessel
-AISVessel currentAisVessel
+updateCursorPosition()
+updateAISVesselInfo()
}
%% ========== UTILITIES ==========
class SettingsManager {
-SharedPreferences prefs
+getUDPPort()
+isUDPEnabled()
+getDataMode()
+saveSettings()
}
class NavigationUtils {
<<utility>>
+calculateDistance()
+calculateBearing()
+formatCoordinates()
+formatSpeed()
}
class GeoUtils {
<<utility>>
+calculateDistance()
+calculateBearing()
+getVesselsInRadius()
+calculateCompassPosition()
+formatCoordinates()
+predictPosition()
+getNavigationStatusCode()
}
%% ========== DATA LAYER ==========
class Repository {
-AppDatabase database
-AISVesselDao aisDao
-VesselDao vesselDao
+saveVessel()
+saveAISVessel()
+getHistoricalVessels()
+cleanupOldData()
+getAISVessels()
+Note: "Persistent Data Manager"
}
class AppDatabase {
<<Room Database>>
+aisVesselDao()
+vesselDao()
}
%% ========== INTERFACES ==========
class NMEAParserListener {
<<interface>>
+onVesselUpdated(Vessel)
+onAISVesselUpdated(AISVessel)
+onParseError(String)
}
class UDPListenerCallback {
<<interface>>
+onDataReceived(String)
+onError(String)
}
class LocationCallback {
<<interface>>
+onLocationUpdated(Location)
+onLocationError(String)
}
class MarkerClickListener {
<<interface>>
+onOwnVesselClick(Vessel)
+onAISVesselClick(AISVessel)
}
%% ========== RELATIONSHIPS ==========
%% Main Activity relationships
MainActivity --> AppController : uses
MainActivity --> MapController : uses
MainActivity --> MapInterface : uses
MainActivity --> CompassView : contains
MainActivity --> CoordinatesDockWidget : contains
MainActivity --> SettingsManager : uses
%% AppController relationships
AppController --> NMEAParser : uses
AppController --> UDPListener : uses
AppController --> AndroidNMEAListener : uses
AppController --> GPSLocationListener : uses
AppController --> MapInterface : uses
AppController --> Vessel : manages
AppController --> AISVessel : manages
AppController ..|> NMEAParserListener : implements
AppController ..|> UDPListenerCallback : implements
AppController ..|> LocationCallback : implements
AppController ..|> MarkerClickListener : implements
%% Map implementations
MapInterface <|.. YandexMapImpl : implements
MapInterface <|.. MapLibreMapImpl : implements
MapInterface <|.. MapForgeImpl : implements
%% Parser relationships
NMEAParser --> NMEAParserListener : notifies
UDPListener --> UDPListenerCallback : notifies
GPSLocationListener --> LocationCallback : notifies
%% UI relationships
BaseDockWidget <|-- CompassView : extends
BaseDockWidget <|-- CoordinatesDockWidget : extends
CompassView --> Vessel : displays
CompassView --> AISVessel : displays
CoordinatesDockWidget --> Vessel : displays
%% Data relationships
MapController --> VesselPathController : uses
VesselPathController --> VesselPathPoint : manages
AppController --> Repository : uses for persistence
Repository --> AppDatabase : uses
Repository --> Vessel : stores
Repository --> AISVessel : stores
%% Utility relationships - CENTRALIZED
AppController --> GeoUtils : uses for calculations
CompassView --> GeoUtils : uses for positioning
CursorOverlay --> GeoUtils : uses for distance/bearing
MapController --> GeoUtils : uses for predictions
NavigationUtils ..> Vessel : formats
NavigationUtils ..> AISVessel : formats
GeoUtils ..> Vessel : calculates
GeoUtils ..> AISVessel : calculates
```
+16
View File
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 92.09 199.81">
<defs>
<style>
.cls-1 {
fill: #d2ff1a;
stroke: #a8cc14;
stroke-miterlimit: 10;
stroke-width: 3px;
}
</style>
</defs>
<g id="_Слой_23" data-name="Слой_23">
<path class="cls-1" d="M44.03,168.47c15.02,0,27.22,12.04,27.49,27l15.45,2.61,3.19-23.41c.89-6.53.41-13.17-1.39-19.5L46.03,5.47,3.33,155.13c-1.81,6.36-2.28,13.02-1.38,19.57l3.2,23.26,11.4-2.5c.27-14.96,12.47-27,27.49-27Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 608 B

+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 66.46 176.77">
<defs>
<style>
.cls-1 {
fill: silver;
stroke: #f3f3f3;
}
.cls-1, .cls-2 {
stroke-miterlimit: 10;
}
.cls-2 {
fill: #e32636;
stroke: #961923;
}
</style>
</defs>
<g id="_Слой_24" data-name="Слой_24">
<path class="cls-2" d="M33.73,64.39c12.24,0,22.33,9.16,23.81,21h8.19L33.23,1.39.73,85.39h9.19c1.48-11.84,11.57-21,23.81-21Z"/>
<path class="cls-1" d="M57.04,91.39c-1.48,11.84-11.57,21-23.81,21s-22.33-9.16-23.81-21H.73l32.5,84,32.5-84h-8.69Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 709 B

+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 233.17 233.87">
<defs>
<style>
.cls-1 {
fill: #fff;
font-family: Roboto-Black, Roboto;
font-size: 31px;
font-weight: 800;
}
.cls-2 {
fill: none;
}
.cls-3 {
stroke: #000;
stroke-miterlimit: 10;
}
.cls-4 {
clip-path: url(#clippath);
}
</style>
<clipPath id="clippath">
<rect class="cls-2" width="233.17" height="233.87"/>
</clipPath>
</defs>
<g id="_Слой_17" data-name="Слой_17">
<g class="cls-4">
<g>
<rect class="cls-3" x=".5" y=".5" width="233" height="53" rx="26.2" ry="26.2"/>
<rect class="cls-3" x=".5" y="60.39" width="233" height="53" rx="26.2" ry="26.2"/>
<rect class="cls-3" x=".5" y="120.28" width="233" height="53" rx="26.2" ry="26.2"/>
<rect class="cls-3" x=".5" y="180.17" width="233" height="53" rx="26.2" ry="26.2"/>
</g>
</g>
<text class="cls-1" transform="translate(78.07 36.01)"><tspan x="0" y="0">MMSI</tspan></text>
<text class="cls-1" transform="translate(80.32 95.1)"><tspan x="0" y="0">COG</tspan></text>
<text class="cls-1" transform="translate(81.32 157.1)"><tspan x="0" y="0">SOG</tspan></text>
<text class="cls-1" transform="translate(98.98 208.1)"><tspan x="0" y="0">...</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+57
View File
@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 233.17 233.87">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: none;
}
.cls-3 {
stroke: #000;
stroke-miterlimit: 10;
}
.cls-4 {
clip-path: url(#clippath);
}
</style>
<clipPath id="clippath">
<rect class="cls-2" width="233.17" height="233.87"/>
</clipPath>
</defs>
<g id="_Слой_17" data-name="Слой_17">
<g class="cls-4">
<g>
<rect class="cls-3" x=".5" y=".5" width="233" height="53" rx="26.2" ry="26.2"/>
<rect class="cls-3" x=".5" y="60.39" width="233" height="53" rx="26.2" ry="26.2"/>
<rect class="cls-3" x=".5" y="120.28" width="233" height="53" rx="26.2" ry="26.2"/>
<rect class="cls-3" x=".5" y="180.17" width="233" height="53" rx="26.2" ry="26.2"/>
</g>
</g>
<g>
<path class="cls-1" d="M89.21,29.73h.09l4.98-15.68h6.77v22.41h-5.12v-14.44l-.09-.02-4.86,14.45h-3.44l-4.77-14.24-.09.02v14.22h-5.12V14.05h6.71l4.93,15.68Z"/>
<path class="cls-1" d="M116.2,29.73h.09l4.98-15.68h6.77v22.41h-5.12v-14.44l-.09-.02-4.86,14.45h-3.44l-4.77-14.24-.09.02v14.22h-5.12V14.05h6.71l4.93,15.68Z"/>
<path class="cls-1" d="M142.92,30.52c0-.8-.26-1.41-.77-1.84s-1.45-.88-2.8-1.36c-2.73-.9-4.77-1.86-6.09-2.89-1.33-1.02-1.99-2.49-1.99-4.41s.77-3.4,2.32-4.56c1.54-1.16,3.51-1.74,5.89-1.74,2.51,0,4.54.6,6.07,1.79,1.53,1.2,2.28,2.88,2.23,5.06l-.03.09h-4.96c0-1.06-.28-1.82-.85-2.3-.57-.48-1.42-.72-2.56-.72-.93,0-1.66.23-2.19.69-.54.46-.8,1.03-.8,1.71s.27,1.18.83,1.58c.55.4,1.58.89,3.08,1.48,2.55.77,4.49,1.71,5.8,2.82,1.31,1.11,1.97,2.63,1.97,4.56s-.76,3.51-2.27,4.62-3.52,1.67-6.02,1.67-4.6-.6-6.33-1.79c-1.73-1.2-2.57-3.08-2.52-5.64l.03-.09h4.98c0,1.3.32,2.23.95,2.78.63.55,1.6.82,2.9.82,1.07,0,1.87-.22,2.39-.65.52-.43.79-1,.79-1.69Z"/>
<path class="cls-1" d="M156.24,36.46h-5.1V14.05h5.1v22.41Z"/>
</g>
<g>
<path class="cls-1" d="M104.15,87.67l.03.09c.04,2.51-.67,4.42-2.13,5.71-1.46,1.3-3.52,1.94-6.2,1.94s-4.91-.83-6.56-2.5c-1.65-1.67-2.48-3.84-2.48-6.54v-4.6c0-2.68.79-4.86,2.38-6.53,1.59-1.67,3.69-2.51,6.3-2.51,2.79,0,4.96.65,6.49,1.95,1.53,1.3,2.27,3.19,2.23,5.68l-.05.09h-4.98c0-1.35-.29-2.32-.86-2.91-.58-.58-1.52-.88-2.83-.88-1.15,0-2.03.46-2.65,1.39-.62.92-.92,2.15-.92,3.69v4.63c0,1.54.34,2.78,1.01,3.71.68.93,1.64,1.39,2.91,1.39,1.17,0,2.02-.29,2.54-.88.52-.58.78-1.56.78-2.94h4.98Z"/>
<path class="cls-1" d="M125.32,86.07c0,2.71-.86,4.95-2.58,6.71-1.72,1.76-3.97,2.64-6.74,2.64s-5.06-.88-6.8-2.64c-1.74-1.76-2.6-4-2.6-6.71v-3.97c0-2.7.87-4.94,2.6-6.71,1.73-1.77,3.99-2.65,6.77-2.65s5.02.88,6.75,2.65c1.74,1.77,2.6,4,2.6,6.71v3.97ZM120.22,82.07c0-1.57-.37-2.87-1.11-3.88-.74-1.01-1.79-1.51-3.14-1.51s-2.44.5-3.17,1.51c-.73,1-1.1,2.3-1.1,3.88v4c0,1.59.37,2.9,1.11,3.91.74,1.01,1.8,1.51,3.19,1.51s2.38-.5,3.12-1.51c.74-1.01,1.1-2.31,1.1-3.91v-4Z"/>
<path class="cls-1" d="M145.85,92.06c-.77.93-1.85,1.72-3.24,2.38-1.39.66-3.18.98-5.37.98-2.73,0-4.96-.84-6.66-2.51-1.71-1.67-2.56-3.85-2.56-6.52v-4.6c0-2.65.83-4.82,2.49-6.51,1.66-1.69,3.8-2.53,6.41-2.53,2.82,0,4.94.63,6.38,1.88,1.44,1.26,2.13,2.97,2.08,5.15l-.03.09h-4.8c0-1.08-.29-1.88-.86-2.41-.58-.52-1.44-.79-2.6-.79s-2.15.47-2.88,1.41-1.09,2.16-1.09,3.66v4.63c0,1.53.36,2.77,1.08,3.7.72.93,1.73,1.4,3.03,1.4.94,0,1.68-.08,2.22-.23.54-.15.97-.35,1.28-.61v-3.94h-3.91v-3.39h9.02v8.73Z"/>
</g>
<g>
<path class="cls-1" d="M98.85,151.26c0-.79-.26-1.39-.77-1.81-.51-.42-1.45-.87-2.8-1.34-2.73-.89-4.77-1.83-6.09-2.84-1.33-1-1.99-2.45-1.99-4.34s.77-3.34,2.32-4.48c1.54-1.14,3.51-1.71,5.89-1.71,2.51,0,4.54.59,6.07,1.76,1.53,1.18,2.28,2.83,2.23,4.97l-.03.09h-4.96c0-1.04-.28-1.79-.85-2.26-.57-.47-1.42-.7-2.56-.7-.93,0-1.66.23-2.19.68-.54.45-.8,1.01-.8,1.68s.27,1.16.83,1.55c.55.39,1.58.88,3.08,1.46,2.55.76,4.49,1.68,5.8,2.77,1.31,1.09,1.97,2.58,1.97,4.48s-.76,3.45-2.27,4.55c-1.51,1.09-3.52,1.64-6.02,1.64s-4.6-.59-6.33-1.76c-1.73-1.18-2.57-3.02-2.52-5.55l.03-.09h4.98c0,1.28.32,2.19.95,2.73.63.54,1.6.81,2.9.81,1.07,0,1.87-.21,2.39-.64.52-.42.79-.98.79-1.67Z"/>
<path class="cls-1" d="M125.07,148.07c0,2.71-.86,4.95-2.58,6.71-1.72,1.76-3.97,2.64-6.74,2.64s-5.06-.88-6.8-2.64c-1.74-1.76-2.6-4-2.6-6.71v-3.97c0-2.7.87-4.94,2.6-6.71,1.73-1.77,3.99-2.65,6.77-2.65s5.02.88,6.75,2.65c1.74,1.77,2.6,4,2.6,6.71v3.97ZM119.97,144.07c0-1.57-.37-2.87-1.11-3.88-.74-1.01-1.79-1.51-3.14-1.51s-2.44.5-3.17,1.51c-.73,1-1.1,2.3-1.1,3.88v4c0,1.59.37,2.9,1.11,3.91.74,1.01,1.8,1.51,3.19,1.51s2.38-.5,3.12-1.51c.74-1.01,1.1-2.31,1.1-3.91v-4Z"/>
<path class="cls-1" d="M145.59,154.06c-.77.93-1.85,1.72-3.24,2.38-1.39.66-3.18.98-5.37.98-2.73,0-4.96-.84-6.66-2.51-1.71-1.67-2.56-3.85-2.56-6.52v-4.6c0-2.65.83-4.82,2.49-6.51,1.66-1.69,3.8-2.53,6.41-2.53,2.82,0,4.94.63,6.38,1.88,1.44,1.26,2.13,2.97,2.08,5.15l-.03.09h-4.8c0-1.08-.29-1.88-.86-2.41-.58-.52-1.44-.79-2.6-.79s-2.15.47-2.88,1.41-1.09,2.16-1.09,3.66v4.63c0,1.53.36,2.77,1.08,3.7.72.93,1.73,1.4,3.03,1.4.94,0,1.68-.08,2.22-.23.54-.15.97-.35,1.28-.61v-3.94h-3.91v-3.39h9.02v8.73Z"/>
</g>
<g>
<path class="cls-1" d="M104.08,208.1h-5.1v-4.3h5.1v4.3Z"/>
<path class="cls-1" d="M113.45,208.1h-5.1v-4.3h5.1v4.3Z"/>
<path class="cls-1" d="M122.82,208.1h-5.1v-4.3h5.1v4.3Z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB