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"> -