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