diff --git a/SVG/compass.svg b/SVG/compass.svg
new file mode 100644
index 0000000..d24b894
--- /dev/null
+++ b/SVG/compass.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/grigowashere/aismap/MainActivity.java b/app/src/main/java/com/grigowashere/aismap/MainActivity.java
index 29742cc..12bed7d 100644
--- a/app/src/main/java/com/grigowashere/aismap/MainActivity.java
+++ b/app/src/main/java/com/grigowashere/aismap/MainActivity.java
@@ -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();
+ }
}
}
diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java b/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java
index e70e25f..5e15a8b 100644
--- a/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java
+++ b/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java
@@ -211,9 +211,11 @@ public class AppController implements
// Восстанавливаем AIS суда
if (aisVessels != null && !aisVessels.isEmpty()) {
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
- for (AISVessel v : aisVessels) {
- Log.d(TAG, " - AIS судно: " + v.getMmsi() + " на " + v.getLatitude() + "," + v.getLongitude());
- uiDataNotifier.onAISVesselChanged(v);
+ 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 {
@@ -255,10 +257,14 @@ public class AppController implements
}
// UDP слушатель запускается в фоновом потоке
- if (isUDPEnabled) {
- executor.execute(() -> {
- udpListener.start();
- });
+ 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 {
// Добавляем новое судно
- aisVessels.add(vessel);
+ synchronized (aisVessels) {
+ aisVessels.add(vessel);
+ }
// Если это новое судно сразу пришло с safety-сообщением — уведомим
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
@@ -661,19 +669,27 @@ public class AppController implements
}
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
- executor.execute(() -> {
+ if (executor != null && !executor.isShutdown()) {
try {
- nmeaParser.parseNMEA(data);
- // Диагностика: логируем каждые 10 секунд
- long now2 = System.currentTimeMillis();
- if (now2 - lastServiceLogTime > 10000) {
- Log.d(TAG, "✅ AppController: UDP NMEA обработано в фоновом потоке");
- lastServiceLogTime = now2;
- }
- } catch (Exception e) {
- Log.e(TAG, "❌ Ошибка парсинга UDP NMEA в фоновом потоке: " + e.getMessage(), e);
+ executor.execute(() -> {
+ try {
+ nmeaParser.parseNMEA(data);
+ // Диагностика: логируем каждые 10 секунд
+ long now2 = System.currentTimeMillis();
+ if (now2 - lastServiceLogTime > 10000) {
+ Log.d(TAG, "✅ AppController: UDP NMEA обработано в фоновом потоке");
+ lastServiceLogTime = now2;
+ }
+ } catch (Exception e) {
+ 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,19 +717,27 @@ public class AppController implements
}
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
- executor.execute(() -> {
+ if (executor != null && !executor.isShutdown()) {
try {
- nmeaParser.parseNMEA(message);
- // Диагностика: логируем каждые 10 секунд
- long now2 = System.currentTimeMillis();
- if (now2 - lastServiceLogTime > 10000) {
- Log.d(TAG, "✅ AppController: NMEA обработано в фоновом потоке");
- lastServiceLogTime = now2;
- }
- } catch (Exception e) {
- Log.e(TAG, "❌ Ошибка парсинга NMEA в фоновом потоке: " + e.getMessage(), e);
+ executor.execute(() -> {
+ try {
+ nmeaParser.parseNMEA(message);
+ // Диагностика: логируем каждые 10 секунд
+ long now2 = System.currentTimeMillis();
+ if (now2 - lastServiceLogTime > 10000) {
+ Log.d(TAG, "✅ AppController: NMEA обработано в фоновом потоке");
+ lastServiceLogTime = now2;
+ }
+ } catch (Exception e) {
+ 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,9 +789,11 @@ public class AppController implements
* Находит AIS судно по MMSI
*/
private AISVessel findAISVesselByMMSI(String mmsi) {
- for (AISVessel vessel : aisVessels) {
- if (mmsi.equals(vessel.getMmsi())) {
- return vessel;
+ synchronized (aisVessels) {
+ for (AISVessel vessel : aisVessels) {
+ if (mmsi.equals(vessel.getMmsi())) {
+ return vessel;
+ }
}
}
return null;
@@ -784,7 +810,9 @@ public class AppController implements
* Получает список AIS судов
*/
public List getAISVessels() {
- return new ArrayList<>(aisVessels);
+ synchronized (aisVessels) {
+ return new ArrayList<>(aisVessels);
+ }
}
/**
@@ -794,7 +822,9 @@ public class AppController implements
Log.i(TAG, "Очищаем AIS суда из контроллера");
// Очищаем локальные данные
- aisVessels.clear();
+ 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();
+ }
}
}
diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java
index b1ea610..96da086 100644
--- a/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java
+++ b/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java
@@ -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
diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java b/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java
index c881ede..9732cd4 100644
--- a/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java
+++ b/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java
@@ -64,6 +64,16 @@ public interface MapInterface {
*/
float getZoom();
+ /**
+ * Установка курса (bearing) карты в градусах (0 = север вверх)
+ */
+ void setBearing(float bearing);
+
+ /**
+ * Текущий курс (bearing) карты в градусах
+ */
+ float getBearing();
+
/**
* Добавление дополнительного слоя
*/
diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java
index 96e7647..14bb490 100644
--- a/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java
+++ b/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java
@@ -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,20 +510,56 @@ public class MapLibreMapImpl implements MapInterface {
@Override
public void setZoom(float zoom) {
- if (maplibreMap == null) return;
- org.maplibre.android.camera.CameraPosition current = maplibreMap.getCameraPosition();
- maplibreMap.setCameraPosition(new org.maplibre.android.camera.CameraPosition.Builder()
- .target(current.target)
- .zoom(zoom)
- .tilt(current.tilt)
- .bearing(current.bearing)
- .build());
+ 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(zoom)
+ .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;
- return (float) maplibreMap.getCameraPosition().zoom;
+ 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,21 +1347,77 @@ 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 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)) {
- if (markerClickListener != null) {
- markerClickListener.onOwnVesselClick(lastOwnVessel);
- }
- } else {
- if (markerClickListener != null) {
- markerClickListener.onAISVesselClick(idToAisVessel.get(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;
+ }
}
}
- return true;
+
+ // Обрабатываем клик по ближайшему судну
+ if (closestId != null && minDistance <= pixelRadius) {
+ if ("own_vessel".equals(closestId)) {
+ if (markerClickListener != null) {
+ markerClickListener.onOwnVesselClick(lastOwnVessel);
+ }
+ } else {
+ if (markerClickListener != null) {
+ 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) {
- // Получаем координаты центра карты
- org.maplibre.android.geometry.LatLng center = maplibreMap.getCameraPosition().target;
- cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
-
- // Проверяем, есть ли AIS судно под курсором
- checkAisVesselUnderCursor(center);
+ 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 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 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 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> 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);
+ }
}
}
diff --git a/app/src/main/java/com/grigowashere/aismap/maps/VesselPathTracker.java b/app/src/main/java/com/grigowashere/aismap/maps/VesselPathTracker.java
index ebd45f0..b9133f3 100644
--- a/app/src/main/java/com/grigowashere/aismap/maps/VesselPathTracker.java
+++ b/app/src/main/java/com/grigowashere/aismap/maps/VesselPathTracker.java
@@ -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()
+ );
}
/**
diff --git a/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java
index 8f12d7a..77038f8 100644
--- a/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java
+++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java
@@ -175,6 +175,25 @@ public class YandexMapImpl implements MapInterface {
public float getZoom() {
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) {
diff --git a/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java b/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java
index 61979f0..bd58a6c 100644
--- a/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java
+++ b/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java
@@ -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 строковый статус
diff --git a/app/src/main/java/com/grigowashere/aismap/utils/NavigationUtils.java b/app/src/main/java/com/grigowashere/aismap/utils/NavigationUtils.java
index 50150f3..1f02778 100644
--- a/app/src/main/java/com/grigowashere/aismap/utils/NavigationUtils.java
+++ b/app/src/main/java/com/grigowashere/aismap/utils/NavigationUtils.java
@@ -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);
}
}
diff --git a/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java b/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java
index 070f1e4..f2f8ea8 100644
--- a/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java
+++ b/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java
@@ -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());
}
}
diff --git a/app/src/main/res/drawable/button_background.xml b/app/src/main/res/drawable/button_background.xml
new file mode 100644
index 0000000..faa4132
--- /dev/null
+++ b/app/src/main/res/drawable/button_background.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/cog.xml b/app/src/main/res/drawable/cog.xml
new file mode 100644
index 0000000..2936c51
--- /dev/null
+++ b/app/src/main/res/drawable/cog.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/compass.xml b/app/src/main/res/drawable/compass.xml
new file mode 100644
index 0000000..6851625
--- /dev/null
+++ b/app/src/main/res/drawable/compass.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ownship.xml b/app/src/main/res/drawable/ownship.xml
new file mode 100644
index 0000000..24efce2
--- /dev/null
+++ b/app/src/main/res/drawable/ownship.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/targetlist.xml b/app/src/main/res/drawable/targetlist.xml
new file mode 100644
index 0000000..50e8ad2
--- /dev/null
+++ b/app/src/main/res/drawable/targetlist.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 525a9d0..cbb1a06 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -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">
-
-
-
+ 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" />
-
diff --git a/app/src/main/res/layout/cursor.xml b/app/src/main/res/layout/cursor.xml
index cc9dc60..a15208c 100644
--- a/app/src/main/res/layout/cursor.xml
+++ b/app/src/main/res/layout/cursor.xml
@@ -13,18 +13,13 @@
android:background="@drawable/cursorcross" />
-
-
+
>
+ +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 {
+ <>
+ -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 {
+ <>
+ +calculateDistance()
+ +calculateBearing()
+ +formatCoordinates()
+ +formatSpeed()
+ }
+
+ class GeoUtils {
+ <>
+ +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 {
+ <>
+ +aisVesselDao()
+ +vesselDao()
+ }
+
+ %% ========== INTERFACES ==========
+ class NMEAParserListener {
+ <>
+ +onVesselUpdated(Vessel)
+ +onAISVesselUpdated(AISVessel)
+ +onParseError(String)
+ }
+
+ class UDPListenerCallback {
+ <>
+ +onDataReceived(String)
+ +onError(String)
+ }
+
+ class LocationCallback {
+ <>
+ +onLocationUpdated(Location)
+ +onLocationError(String)
+ }
+
+ class MarkerClickListener {
+ <>
+ +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
+```
diff --git a/rawAssets/SVG/SVG/ownShip.svg b/rawAssets/SVG/SVG/ownShip.svg
new file mode 100644
index 0000000..407f0f3
--- /dev/null
+++ b/rawAssets/SVG/SVG/ownShip.svg
@@ -0,0 +1,16 @@
+
+
\ No newline at end of file
diff --git a/rawAssets/SVG/compass.svg b/rawAssets/SVG/compass.svg
new file mode 100644
index 0000000..d24b894
--- /dev/null
+++ b/rawAssets/SVG/compass.svg
@@ -0,0 +1,24 @@
+
+
\ No newline at end of file
diff --git a/rawAssets/SVG/targetList.svg b/rawAssets/SVG/targetList.svg
new file mode 100644
index 0000000..c1cc506
--- /dev/null
+++ b/rawAssets/SVG/targetList.svg
@@ -0,0 +1,43 @@
+
+
\ No newline at end of file
diff --git a/rawAssets/SVG/targetList_1.svg b/rawAssets/SVG/targetList_1.svg
new file mode 100644
index 0000000..23481f3
--- /dev/null
+++ b/rawAssets/SVG/targetList_1.svg
@@ -0,0 +1,57 @@
+
+
\ No newline at end of file