generated from Grigo/AndroidTemplate
Initial commit: AIS Map Android application
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,615 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.LocationManager;
|
||||
import android.location.OnNmeaMessageListener;
|
||||
import android.location.LocationListener;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.GpsStatus.Listener;
|
||||
import android.os.Looper;
|
||||
import android.os.Handler;
|
||||
|
||||
|
||||
/**
|
||||
* Контроллер для Android NMEA Listener
|
||||
* Использует встроенный GPS для получения NMEA сообщений
|
||||
*/
|
||||
public class AndroidNMEAListener implements OnNmeaMessageListener {
|
||||
|
||||
private static final String TAG = "AndroidNMEAListener";
|
||||
|
||||
private Context context;
|
||||
private LocationManager locationManager;
|
||||
private NMEAMessageCallback callback;
|
||||
private boolean isListening;
|
||||
private int satelliteCount;
|
||||
private LocationListener locationListener;
|
||||
private GpsStatus.Listener gpsStatusListener;
|
||||
private GnssStatus.Callback gnssStatusCallback;
|
||||
private Handler activationHandler;
|
||||
private boolean hasReceivedNMEA;
|
||||
|
||||
public interface NMEAMessageCallback {
|
||||
void onNMEAMessage(String message, long timestamp);
|
||||
void onGPSStatusChanged(int status);
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public AndroidNMEAListener(Context context) {
|
||||
this.context = context;
|
||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
this.isListening = false;
|
||||
this.activationHandler = new Handler(Looper.getMainLooper());
|
||||
this.hasReceivedNMEA = false;
|
||||
}
|
||||
|
||||
public void setCallback(NMEAMessageCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает прослушивание NMEA сообщений
|
||||
*/
|
||||
public boolean startListening() {
|
||||
if (isListening) {
|
||||
//Log.w(TAG, "NMEA слушатель уже запущен");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (locationManager == null) {
|
||||
//Log.e(TAG, "LocationManager недоступен");
|
||||
if (callback != null) {
|
||||
callback.onError("LocationManager недоступен");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем все доступные провайдеры
|
||||
//Log.i(TAG, "=== ДИАГНОСТИКА GPS ===");
|
||||
//Log.i(TAG, "Доступные провайдеры:");
|
||||
for (String provider : locationManager.getAllProviders()) {
|
||||
boolean enabled = locationManager.isProviderEnabled(provider);
|
||||
//Log.i(TAG, " " + provider + ": " + (enabled ? "включен" : "отключен"));
|
||||
}
|
||||
|
||||
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
//Log.w(TAG, "GPS провайдер отключен");
|
||||
if (callback != null) {
|
||||
callback.onError("GPS провайдер отключен");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем разрешения
|
||||
if (context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
//Log.e(TAG, "Нет разрешения ACCESS_FINE_LOCATION");
|
||||
if (callback != null) {
|
||||
callback.onError("Нет разрешения ACCESS_FINE_LOCATION");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//Log.i(TAG, "Разрешения GPS получены");
|
||||
|
||||
try {
|
||||
//Log.i(TAG, "=== РЕГИСТРАЦИЯ СЛУШАТЕЛЕЙ ===");
|
||||
|
||||
// Регистрируем NMEA слушатель с минимальным интервалом
|
||||
locationManager.addNmeaListener(this, new Handler(Looper.getMainLooper()));
|
||||
//Log.i(TAG, "✅ NMEA слушатель зарегистрирован");
|
||||
|
||||
// Регистрируем GPS статус слушатель (для старых версий Android)
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
gpsStatusListener = new GpsStatus.Listener() {
|
||||
@Override
|
||||
public void onGpsStatusChanged(int event) {
|
||||
String eventName = getGpsEventName(event);
|
||||
//Log.d(TAG, "GPS статус изменился: " + event + " (" + eventName + ")");
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(event);
|
||||
}
|
||||
|
||||
// При получении первого фиксирования запрашиваем обновления
|
||||
if (event == GpsStatus.GPS_EVENT_FIRST_FIX) {
|
||||
//Log.i(TAG, "🎯 Получено первое GPS фиксирование, активируем NMEA");
|
||||
requestLocationUpdates();
|
||||
}
|
||||
}
|
||||
};
|
||||
locationManager.addGpsStatusListener(gpsStatusListener);
|
||||
//Log.i(TAG, "✅ GPS статус слушатель зарегистрирован (старый API)");
|
||||
}
|
||||
|
||||
// Регистрируем GNSS статус callback (для новых версий Android)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
gnssStatusCallback = new GnssStatus.Callback() {
|
||||
@Override
|
||||
public void onStarted() {
|
||||
//Log.i(TAG, "🚀 GNSS начал работу");
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(1); // GPS_EVENT_STARTED
|
||||
}
|
||||
// При старте GNSS сразу запрашиваем обновления
|
||||
requestLocationUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopped() {
|
||||
//Log.i(TAG, "⏹️ GNSS остановлен");
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(2); // GPS_EVENT_STOPPED
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFirstFix(int ttffMillis) {
|
||||
//Log.i(TAG, "🎯 GNSS получил первое фиксирование за " + ttffMillis + "мс");
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(3); // GPS_EVENT_FIRST_FIX
|
||||
}
|
||||
// При первом фиксировании также запрашиваем обновления
|
||||
requestLocationUpdates();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
int count = status.getSatelliteCount();
|
||||
//Log.d(TAG, "GNSS статус спутников изменился, количество: " + count);
|
||||
|
||||
// Подсчитываем количество спутников
|
||||
satelliteCount = count;
|
||||
|
||||
// Логируем детали по спутникам
|
||||
for (int i = 0; i < count; i++) {
|
||||
int constellationType = status.getConstellationType(i);
|
||||
float cn0DbHz = status.getCn0DbHz(i);
|
||||
boolean usedInFix = status.usedInFix(i);
|
||||
String constellationName = getConstellationName(constellationType);
|
||||
//Log.d(TAG, " Спутник " + i + ": " + constellationName +
|
||||
// ", сигнал=" + cn0DbHz + "dB-Hz, используется=" + usedInFix);
|
||||
}
|
||||
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(4); // GPS_EVENT_SATELLITE_STATUS
|
||||
}
|
||||
|
||||
// Если есть спутники, но нет NMEA - принудительно активируем GPS
|
||||
if (count > 0 && !isListening) {
|
||||
//Log.i(TAG, "Спутники видны, но слушатель не активен. Активируем GPS...");
|
||||
// requestLocationUpdates();
|
||||
}
|
||||
}
|
||||
};
|
||||
locationManager.registerGnssStatusCallback(gnssStatusCallback, new Handler(Looper.getMainLooper()));
|
||||
//Log.i(TAG, "✅ GNSS статус callback зарегистрирован (новый API)");
|
||||
}
|
||||
|
||||
// Для старых версий Android сразу запрашиваем обновления
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
//Log.i(TAG, "📱 Старая версия Android, сразу запрашиваем location updates");
|
||||
requestLocationUpdates();
|
||||
}
|
||||
|
||||
// ВАЖНО: Принудительно активируем GPS для получения NMEA
|
||||
//Log.i(TAG, "🔧 Принудительно активируем GPS для получения NMEA...");
|
||||
requestLocationUpdates();
|
||||
|
||||
// Запускаем таймер для принудительной повторной активации
|
||||
startActivationTimer();
|
||||
|
||||
isListening = true;
|
||||
//Log.i(TAG, "🎉 NMEA слушатель успешно запущен!");
|
||||
//Log.i(TAG, "💡 Теперь GPS должен начать отправлять NMEA сообщения");
|
||||
return true;
|
||||
|
||||
} catch (SecurityException e) {
|
||||
//Log.e(TAG, "❌ Нет разрешений для доступа к GPS: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onError("Нет разрешений для доступа к GPS");
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
//Log.e(TAG, "❌ Ошибка при запуске NMEA слушателя: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
if (callback != null) {
|
||||
callback.onError("Ошибка запуска: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрашивает обновления локации для активации GPS
|
||||
*/
|
||||
private void requestLocationUpdates() {
|
||||
try {
|
||||
//Log.i(TAG, "🔧 Запрашиваем обновления локации для активации NMEA...");
|
||||
|
||||
// Создаем слушатель локации с минимальными интервалами
|
||||
locationListener = new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(android.location.Location location) {
|
||||
//Log.d(TAG, "📍 Location обновлен: " + location.getLatitude() + ", " + location.getLongitude());
|
||||
//Log.d(TAG, "📍 Точность: " + location.getAccuracy() + "м, время: " + location.getTime());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, android.os.Bundle extras) {
|
||||
String statusName = getLocationStatusName(status);
|
||||
//Log.d(TAG, "📍 Location статус изменился: " + provider + " = " + status + " (" + statusName + ")");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
//Log.i(TAG, "📍 Location провайдер включен: " + provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
//Log.w(TAG, "📍 Location провайдер отключен: " + provider);
|
||||
}
|
||||
};
|
||||
|
||||
// Запрашиваем обновления с минимальными интервалами для активации GPS
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
100L, // минимальный интервал в мс (long вместо int)
|
||||
0.0f, // минимальное расстояние в метрах (float вместо int)
|
||||
locationListener,
|
||||
Looper.getMainLooper() // Looper вместо Handler
|
||||
);
|
||||
|
||||
//Log.i(TAG, "✅ Location updates запрошены с минимальным интервалом (100мс)");
|
||||
|
||||
// Дополнительно запрашиваем одиночное обновление для принудительной активации
|
||||
try {
|
||||
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER,
|
||||
new LocationListener() {
|
||||
@Override public void onLocationChanged(android.location.Location location) {
|
||||
//Log.i(TAG, "🎯 Одиночное обновление получено: " + location.getLatitude() + ", " + location.getLongitude());
|
||||
}
|
||||
@Override public void onStatusChanged(String provider, int status, android.os.Bundle extras) {}
|
||||
@Override public void onProviderEnabled(String provider) {}
|
||||
@Override public void onProviderDisabled(String provider) {}
|
||||
}, Looper.getMainLooper()); // Looper вместо Handler
|
||||
//Log.i(TAG, "✅ Одиночное обновление запрошено");
|
||||
} catch (Exception e) {
|
||||
//Log.w(TAG, "⚠️ Не удалось запросить одиночное обновление: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Дополнительно запрашиваем обновления от всех доступных провайдеров
|
||||
try {
|
||||
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.NETWORK_PROVIDER,
|
||||
1000L, // 1 секунда
|
||||
0.0f,
|
||||
locationListener,
|
||||
Looper.getMainLooper()
|
||||
);
|
||||
//Log.i(TAG, "✅ Network провайдер также активирован");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//Log.w(TAG, "⚠️ Не удалось активировать network провайдер: " + e.getMessage());
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
//Log.e(TAG, "❌ Ошибка при запросе location updates: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает прослушивание NMEA сообщений
|
||||
*/
|
||||
public void stopListening() {
|
||||
if (!isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (locationManager != null) {
|
||||
// Убираем NMEA слушатель
|
||||
locationManager.removeNmeaListener(this);
|
||||
|
||||
// Убираем GNSS статус callback
|
||||
if (gnssStatusCallback != null) {
|
||||
locationManager.unregisterGnssStatusCallback(gnssStatusCallback);
|
||||
gnssStatusCallback = null;
|
||||
}
|
||||
|
||||
// Убираем GPS статус слушатель (старый API)
|
||||
if (gpsStatusListener != null) {
|
||||
locationManager.removeGpsStatusListener(gpsStatusListener);
|
||||
gpsStatusListener = null;
|
||||
}
|
||||
|
||||
// Убираем Location updates
|
||||
if (locationListener != null) {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
locationListener = null;
|
||||
//Log.i(TAG, "Location updates остановлены");
|
||||
}
|
||||
}
|
||||
|
||||
// Останавливаем таймер активации
|
||||
stopActivationTimer();
|
||||
|
||||
isListening = false;
|
||||
hasReceivedNMEA = false;
|
||||
//Log.i(TAG, "NMEA слушатель остановлен");
|
||||
|
||||
} catch (Exception e) {
|
||||
//Log.e(TAG, "Ошибка при остановке NMEA слушателя: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback для NMEA сообщений (API 24+)
|
||||
*/
|
||||
@Override
|
||||
public void onNmeaMessage(String message, long timestamp) {
|
||||
if (message == null || message.trim().isEmpty()) {
|
||||
//Log.w(TAG, "⚠️ Получено пустое NMEA сообщение");
|
||||
return;
|
||||
}
|
||||
|
||||
// Отмечаем, что NMEA получено
|
||||
hasReceivedNMEA = true;
|
||||
|
||||
// Анализируем тип NMEA сообщения
|
||||
String messageType = getNMEAMessageType(message);
|
||||
String LogPrefix = "🎯 NMEA [" + messageType + "]";
|
||||
|
||||
//Log.i(TAG, //LogPrefix + " получено: " + message);
|
||||
//Log.d(TAG, //LogPrefix + " timestamp: " + timestamp + " (" + new java.util.Date(timestamp) + ")");
|
||||
|
||||
// Дополнительная диагностика для RMC сообщений
|
||||
if ("RMC".equals(messageType)) {
|
||||
//Log.i(TAG, //LogPrefix + " 🚢 RMC сообщение получено - GPS активен!");
|
||||
// Можно добавить парсинг RMC для получения дополнительной информации
|
||||
parseRMC(message);
|
||||
}
|
||||
|
||||
// Останавливаем таймер активации, так как NMEA приходит
|
||||
stopActivationTimer();
|
||||
|
||||
if (callback != null) {
|
||||
//Log.d(TAG, //LogPrefix + " Отправляем сообщение в callback");
|
||||
callback.onNMEAMessage(message, timestamp);
|
||||
} else {
|
||||
//Log.w(TAG, //LogPrefix + " ❌ Callback не установлен!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Определяет тип NMEA сообщения
|
||||
*/
|
||||
private String getNMEAMessageType(String message) {
|
||||
if (message == null || message.length() < 6) {
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// NMEA сообщения начинаются с $ и содержат тип после $
|
||||
if (message.startsWith("$")) {
|
||||
String[] parts = message.split(",");
|
||||
if (parts.length > 0) {
|
||||
// Убираем $ и берем первые 3 символа
|
||||
String type = parts[0].substring(1);
|
||||
if (type.length() >= 3) {
|
||||
return type.substring(0, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
/**
|
||||
* Парсит RMC сообщение для диагностики
|
||||
*/
|
||||
private void parseRMC(String message) {
|
||||
try {
|
||||
String[] parts = message.split(",");
|
||||
if (parts.length >= 12) {
|
||||
String time = parts[1];
|
||||
String status = parts[2];
|
||||
String lat = parts[3];
|
||||
String latDir = parts[4];
|
||||
String lon = parts[5];
|
||||
String lonDir = parts[6];
|
||||
String speed = parts[7];
|
||||
String course = parts[8];
|
||||
String date = parts[9];
|
||||
|
||||
//Log.d(TAG, "🚢 RMC детали:");
|
||||
//Log.d(TAG, " Время: " + time + ", Дата: " + date);
|
||||
//Log.d(TAG, " Статус: " + (status.equals("A") ? "Активен" : "Неактивен"));
|
||||
//Log.d(TAG, " Координаты: " + lat + latDir + ", " + lon + lonDir);
|
||||
//Log.d(TAG, " Скорость: " + speed + " узлов, Курс: " + course + "°");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//Log.w(TAG, "⚠️ Не удалось распарсить RMC: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, запущен ли слушатель
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return isListening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет доступность GPS
|
||||
*/
|
||||
public boolean isGPSAvailable() {
|
||||
if (locationManager == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
|
||||
boolean networkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
|
||||
boolean passiveEnabled = locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER);
|
||||
|
||||
//Log.d(TAG, "GPS провайдер: " + (gpsEnabled ? "включен" : "отключен"));
|
||||
//Log.d(TAG, "Network провайдер: " + (networkEnabled ? "включен" : "отключен"));
|
||||
//Log.d(TAG, "Passive провайдер: " + (passiveEnabled ? "включен" : "отключен"));
|
||||
|
||||
return gpsEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает детальную информацию о состоянии GPS
|
||||
*/
|
||||
public String getGPSStatusInfo() {
|
||||
if (locationManager == null) {
|
||||
return "LocationManager недоступен";
|
||||
}
|
||||
|
||||
StringBuilder info = new StringBuilder();
|
||||
info.append("GPS провайдер: ").append(locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ? "включен" : "отключен").append("\n");
|
||||
info.append("Network провайдер: ").append(locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ? "включен" : "отключен").append("\n");
|
||||
info.append("Passive провайдер: ").append(locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER) ? "включен" : "отключен").append("\n");
|
||||
info.append("Спутников: ").append(satelliteCount).append("\n");
|
||||
info.append("Слушатель активен: ").append(isListening ? "да" : "нет");
|
||||
|
||||
return info.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно активирует GPS для получения NMEA
|
||||
*/
|
||||
public void forceGPSActivation() {
|
||||
if (!isListening) {
|
||||
//Log.w(TAG, "Слушатель не запущен, запускаем...");
|
||||
startListening();
|
||||
return;
|
||||
}
|
||||
|
||||
//Log.i(TAG, "Принудительно активируем GPS...");
|
||||
|
||||
// Запрашиваем обновления локации с минимальными интервалами
|
||||
try {
|
||||
if (locationListener != null) {
|
||||
locationManager.removeUpdates(locationListener);
|
||||
}
|
||||
|
||||
requestLocationUpdates();
|
||||
|
||||
// Дополнительно запрашиваем одиночное обновление
|
||||
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER,
|
||||
new LocationListener() {
|
||||
@Override public void onLocationChanged(android.location.Location location) {
|
||||
//Log.i(TAG, "Одиночное обновление получено: " + location.getLatitude() + ", " + location.getLongitude());
|
||||
}
|
||||
@Override public void onStatusChanged(String provider, int status, android.os.Bundle extras) {}
|
||||
@Override public void onProviderEnabled(String provider) {}
|
||||
@Override public void onProviderDisabled(String provider) {}
|
||||
}, Looper.getMainLooper()); // Looper вместо Handler
|
||||
|
||||
//Log.i(TAG, "Принудительная активация GPS выполнена");
|
||||
|
||||
} catch (Exception e) {
|
||||
//Log.e(TAG, "Ошибка при принудительной активации GPS: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает таймер для принудительной повторной активации GPS
|
||||
*/
|
||||
private void startActivationTimer() {
|
||||
// Останавливаем предыдущий таймер
|
||||
activationHandler.removeCallbacksAndMessages(null);
|
||||
|
||||
// Запускаем таймер на 5 секунд
|
||||
activationHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isListening && !hasReceivedNMEA) {
|
||||
//Log.w(TAG, "⏰ Таймер активации: NMEA не получено, принудительно активируем GPS...");
|
||||
forceGPSActivation();
|
||||
|
||||
// Повторяем каждые 10 секунд, пока не получим NMEA
|
||||
activationHandler.postDelayed(this, 10000);
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
//Log.i(TAG, "⏰ Таймер активации GPS запущен (5 сек)");
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает таймер активации
|
||||
*/
|
||||
private void stopActivationTimer() {
|
||||
activationHandler.removeCallbacksAndMessages(null);
|
||||
//Log.d(TAG, "⏰ Таймер активации GPS остановлен");
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает количество спутников
|
||||
*/
|
||||
public int getSatelliteCount() {
|
||||
return satelliteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (isListening) {
|
||||
stopListening();
|
||||
}
|
||||
|
||||
// Останавливаем таймер активации
|
||||
stopActivationTimer();
|
||||
|
||||
locationManager = null;
|
||||
activationHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает название GPS события
|
||||
*/
|
||||
private String getGpsEventName(int event) {
|
||||
switch (event) {
|
||||
case GpsStatus.GPS_EVENT_STARTED: return "GPS_EVENT_STARTED";
|
||||
case GpsStatus.GPS_EVENT_STOPPED: return "GPS_EVENT_STOPPED";
|
||||
case GpsStatus.GPS_EVENT_FIRST_FIX: return "GPS_EVENT_FIRST_FIX";
|
||||
case GpsStatus.GPS_EVENT_SATELLITE_STATUS: return "GPS_EVENT_SATELLITE_STATUS";
|
||||
default: return "UNKNOWN(" + event + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает название созвездия спутников
|
||||
*/
|
||||
private String getConstellationName(int constellationType) {
|
||||
switch (constellationType) {
|
||||
case GnssStatus.CONSTELLATION_GPS: return "GPS";
|
||||
case GnssStatus.CONSTELLATION_GLONASS: return "GLONASS";
|
||||
case GnssStatus.CONSTELLATION_BEIDOU: return "BeiDou";
|
||||
case GnssStatus.CONSTELLATION_GALILEO: return "Galileo";
|
||||
case GnssStatus.CONSTELLATION_QZSS: return "QZSS";
|
||||
case GnssStatus.CONSTELLATION_SBAS: return "SBAS";
|
||||
case GnssStatus.CONSTELLATION_IRNSS: return "IRNSS";
|
||||
default: return "Unknown(" + constellationType + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает название статуса локации
|
||||
*/
|
||||
private String getLocationStatusName(int status) {
|
||||
switch (status) {
|
||||
case android.location.LocationProvider.AVAILABLE: return "AVAILABLE";
|
||||
case android.location.LocationProvider.TEMPORARILY_UNAVAILABLE: return "TEMPORARILY_UNAVAILABLE";
|
||||
case android.location.LocationProvider.OUT_OF_SERVICE: return "OUT_OF_SERVICE";
|
||||
default: return "UNKNOWN(" + status + ")";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,595 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.maps.MapInterface;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Главный контроллер приложения
|
||||
* Координирует работу всех компонентов
|
||||
* Использует гибридный подход: координаты через Location API, остальное через NMEA
|
||||
*/
|
||||
public class AppController implements
|
||||
NMEAParser.NMEAParserListener,
|
||||
UDPListener.UDPListenerCallback,
|
||||
AndroidNMEAListener.NMEAMessageCallback,
|
||||
GPSLocationListener.LocationCallback,
|
||||
MapInterface.MarkerClickListener {
|
||||
|
||||
private static final String TAG = "AppController";
|
||||
|
||||
private Context context;
|
||||
private NMEAParser nmeaParser;
|
||||
private UDPListener udpListener;
|
||||
private AndroidNMEAListener androidNmeaListener;
|
||||
private GPSLocationListener gpsLocationListener;
|
||||
private MapInterface mapInterface;
|
||||
|
||||
private Vessel ownVessel;
|
||||
private List<AISVessel> aisVessels;
|
||||
private ExecutorService executor;
|
||||
|
||||
private boolean isUDPEnabled;
|
||||
private boolean isAndroidNMEAEnabled;
|
||||
private boolean isGPSLocationEnabled;
|
||||
|
||||
// Callback для обновления UI
|
||||
private UIUpdateCallback uiUpdateCallback;
|
||||
|
||||
public interface UIUpdateCallback {
|
||||
void onVesselPositionUpdated(Vessel vessel);
|
||||
void onGPSQualityUpdated(Vessel vessel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Расширенный интерфейс для дополнительных UI событий
|
||||
*/
|
||||
public interface ExtendedUIUpdateCallback extends UIUpdateCallback {
|
||||
void onShowOwnVesselBottomSheet();
|
||||
void onShowAISVesselInfo(AISVessel vessel);
|
||||
void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels);
|
||||
}
|
||||
|
||||
public AppController(Context context) {
|
||||
this.context = context;
|
||||
this.ownVessel = new Vessel();
|
||||
this.aisVessels = new ArrayList<>();
|
||||
this.executor = Executors.newCachedThreadPool();
|
||||
|
||||
initializeControllers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует все контроллеры
|
||||
*/
|
||||
private void initializeControllers() {
|
||||
// Инициализация парсера NMEA
|
||||
nmeaParser = new NMEAParser();
|
||||
nmeaParser.setListener(this);
|
||||
|
||||
// Инициализация GPS Location Listener (для координат)
|
||||
gpsLocationListener = new GPSLocationListener(context);
|
||||
gpsLocationListener.setCallback(this);
|
||||
|
||||
// Связываем NMEA парсер с GPS Location Listener для гибридного режима
|
||||
nmeaParser.setGPSLocationListener(gpsLocationListener);
|
||||
nmeaParser.setHybridMode(true);
|
||||
|
||||
// Инициализация UDP слушателя (порт 10110 - стандартный для AIS)
|
||||
udpListener = new UDPListener(10110);
|
||||
udpListener.setCallback(this);
|
||||
|
||||
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
|
||||
androidNmeaListener = new AndroidNMEAListener(context);
|
||||
androidNmeaListener.setCallback(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает интерфейс карты
|
||||
*/
|
||||
public void setMapInterface(MapInterface mapInterface) {
|
||||
Log.i(TAG, "setMapInterface вызван: " + (mapInterface != null ? "mapInterface установлен" : "mapInterface == null"));
|
||||
this.mapInterface = mapInterface;
|
||||
if (mapInterface != null) {
|
||||
Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface");
|
||||
mapInterface.setMarkerClickListener(this);
|
||||
Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает callback для обновления UI
|
||||
*/
|
||||
public void setUIUpdateCallback(UIUpdateCallback callback) {
|
||||
this.uiUpdateCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает все слушатели
|
||||
*/
|
||||
public void startAllListeners() {
|
||||
// GPS Location Listener запускается в главном потоке
|
||||
if (isGPSLocationEnabled) {
|
||||
gpsLocationListener.startListening();
|
||||
}
|
||||
|
||||
// Android NMEA слушатель должен запускаться в главном потоке
|
||||
if (isAndroidNMEAEnabled) {
|
||||
androidNmeaListener.startListening();
|
||||
}
|
||||
|
||||
// UDP слушатель запускается в фоновом потоке
|
||||
if (isUDPEnabled) {
|
||||
executor.execute(() -> {
|
||||
udpListener.start();
|
||||
});
|
||||
}
|
||||
|
||||
// Тестируем NMEA парсер (временно)
|
||||
testNMEAParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Тестирует NMEA парсер (временно для отладки)
|
||||
*/
|
||||
private void testNMEAParser() {
|
||||
Log.i(TAG, "Тестируем NMEA парсер...");
|
||||
|
||||
// Тестовые NMEA сообщения
|
||||
String[] testMessages = {
|
||||
"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
|
||||
"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A",
|
||||
"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48",
|
||||
"$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.8,1.0*3E"
|
||||
};
|
||||
|
||||
for (String message : testMessages) {
|
||||
Log.i(TAG, "Тестируем сообщение: " + message);
|
||||
nmeaParser.parseNMEA(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает все слушатели
|
||||
*/
|
||||
public void stopAllListeners() {
|
||||
executor.execute(() -> {
|
||||
udpListener.stop();
|
||||
androidNmeaListener.stopListening();
|
||||
gpsLocationListener.stopListening();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает UDP слушатель
|
||||
*/
|
||||
public void setUDPEnabled(boolean enabled) {
|
||||
this.isUDPEnabled = enabled;
|
||||
if (enabled && !udpListener.isRunning()) {
|
||||
udpListener.start();
|
||||
} else if (!enabled && udpListener.isRunning()) {
|
||||
udpListener.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает Android NMEA слушатель
|
||||
*/
|
||||
public void setAndroidNMEAEnabled(boolean enabled) {
|
||||
Log.i(TAG, "🔄 setAndroidNMEAEnabled: " + enabled);
|
||||
this.isAndroidNMEAEnabled = enabled;
|
||||
|
||||
// Android NMEA слушатель управляется в главном потоке
|
||||
if (enabled && !androidNmeaListener.isListening()) {
|
||||
Log.i(TAG, "🚀 Запускаем Android NMEA слушатель...");
|
||||
boolean success = androidNmeaListener.startListening();
|
||||
if (success) {
|
||||
Log.i(TAG, "✅ Android NMEA слушатель успешно запущен");
|
||||
} else {
|
||||
Log.e(TAG, "❌ Не удалось запустить Android NMEA слушатель");
|
||||
}
|
||||
} else if (!enabled && androidNmeaListener.isListening()) {
|
||||
Log.i(TAG, "⏹️ Останавливаем Android NMEA слушатель...");
|
||||
androidNmeaListener.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает GPS Location слушатель
|
||||
*/
|
||||
public void setGPSLocationEnabled(boolean enabled) {
|
||||
Log.i(TAG, "🔄 setGPSLocationEnabled: " + enabled);
|
||||
this.isGPSLocationEnabled = enabled;
|
||||
|
||||
if (enabled && !gpsLocationListener.isListening()) {
|
||||
Log.i(TAG, "🚀 Запускаем GPS Location слушатель...");
|
||||
boolean success = gpsLocationListener.startListening();
|
||||
if (success) {
|
||||
Log.i(TAG, "✅ GPS Location слушатель успешно запущен");
|
||||
} else {
|
||||
Log.e(TAG, "❌ Не удалось запустить GPS Location слушатель");
|
||||
}
|
||||
} else if (!enabled && gpsLocationListener.isListening()) {
|
||||
Log.i(TAG, "⏹️ Останавливаем GPS Location слушатель...");
|
||||
gpsLocationListener.stopListening();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает UDP порт
|
||||
*/
|
||||
public void setUDPPort(int port) {
|
||||
udpListener.setPort(port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет данные по UDP
|
||||
*/
|
||||
public void sendUDPData(String data, String address, int port) {
|
||||
udpListener.sendData(data, address, port);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли UDP слушатель
|
||||
*/
|
||||
public boolean isUDPEnabled() {
|
||||
return isUDPEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли Android NMEA слушатель
|
||||
*/
|
||||
public boolean isAndroidNMEAEnabled() {
|
||||
return isAndroidNMEAEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли GPS Location слушатель
|
||||
*/
|
||||
public boolean isGPSLocationEnabled() {
|
||||
return isGPSLocationEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет данные нашего судна при клике по маркеру
|
||||
*/
|
||||
private void updateOwnVesselData(Vessel vessel) {
|
||||
if (vessel != null) {
|
||||
// Обновляем только те данные, которые могут быть актуальными
|
||||
// Координаты и основная информация уже обновляются через GPS
|
||||
if (vessel.getCourse() > 0) {
|
||||
ownVessel.setCourse(vessel.getCourse());
|
||||
updateCompass(); // Обновляем компас при изменении курса
|
||||
}
|
||||
if (vessel.getSpeed() > 0) {
|
||||
ownVessel.setSpeed(vessel.getSpeed());
|
||||
}
|
||||
if (vessel.getSatellites() > 0) {
|
||||
ownVessel.setSatellites(vessel.getSatellites());
|
||||
}
|
||||
if (vessel.getAltitude() != 0) {
|
||||
ownVessel.setAltitude(vessel.getAltitude());
|
||||
}
|
||||
if (vessel.getPdop() > 0) {
|
||||
ownVessel.setPdop(vessel.getPdop());
|
||||
ownVessel.setHdop(vessel.getHdop());
|
||||
ownVessel.setVdop(vessel.getVdop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Реализация LocationCallback (GPS Location Listener)
|
||||
|
||||
@Override
|
||||
public void onLocationUpdated(Vessel vessel) {
|
||||
Log.i(TAG, "📍 GPS Location обновлен: lat=" + vessel.getLatitude() +
|
||||
", lon=" + vessel.getLongitude() +
|
||||
", accuracy=" + vessel.getAccuracy() + "м");
|
||||
|
||||
// Обновляем координаты нашего судна
|
||||
ownVessel.setLatitude(vessel.getLatitude());
|
||||
ownVessel.setLongitude(vessel.getLongitude());
|
||||
ownVessel.setAccuracy(vessel.getAccuracy());
|
||||
ownVessel.setFixTime(vessel.getFixTime());
|
||||
ownVessel.setFixQuality(vessel.getFixQuality());
|
||||
|
||||
// Обновляем UI через callback
|
||||
if (uiUpdateCallback != null) {
|
||||
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
|
||||
}
|
||||
|
||||
// Обновляем карту в главном потоке
|
||||
if (mapInterface != null) {
|
||||
Log.i(TAG, "Обновляем позицию на карте...");
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
try {
|
||||
Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition...");
|
||||
mapInterface.updateOwnVesselPosition(ownVessel);
|
||||
Log.i(TAG, "Позиция на карте обновлена");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка обновления позиции на карте: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGPSStatusChanged(int status) {
|
||||
Log.i(TAG, "GPS статус изменился: " + status);
|
||||
}
|
||||
|
||||
// Реализация NMEAParserListener
|
||||
|
||||
@Override
|
||||
public void onVesselUpdated(Vessel vessel) {
|
||||
// В гибридном режиме обновляем только дополнительные данные
|
||||
if (vessel.getCourse() > 0) {
|
||||
ownVessel.setCourse(vessel.getCourse());
|
||||
updateCompass(); // Обновляем компас при изменении курса
|
||||
}
|
||||
if (vessel.getSpeed() > 0) {
|
||||
ownVessel.setSpeed(vessel.getSpeed());
|
||||
}
|
||||
if (vessel.getSatellites() > 0) {
|
||||
ownVessel.setSatellites(vessel.getSatellites());
|
||||
}
|
||||
if (vessel.getAltitude() != 0) {
|
||||
ownVessel.setAltitude(vessel.getAltitude());
|
||||
}
|
||||
|
||||
Log.i(TAG, "NMEA данные обновлены: course=" + vessel.getCourse() +
|
||||
", speed=" + vessel.getSpeed() +
|
||||
", satellites=" + vessel.getSatellites());
|
||||
|
||||
// Обновляем UI
|
||||
if (uiUpdateCallback != null) {
|
||||
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDOPUpdated(double pdop, double hdop, double vdop) {
|
||||
Log.i(TAG, "📊 DOP обновлен: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
|
||||
|
||||
// Обновляем DOP значения
|
||||
ownVessel.setPdop(pdop);
|
||||
ownVessel.setHdop(hdop);
|
||||
ownVessel.setVdop(vdop);
|
||||
|
||||
// Обновляем UI
|
||||
if (uiUpdateCallback != null) {
|
||||
uiUpdateCallback.onGPSQualityUpdated(ownVessel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAISVesselUpdated(AISVessel vessel) {
|
||||
// Проверяем, есть ли уже такое судно
|
||||
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
|
||||
|
||||
if (existingVessel != null) {
|
||||
// Обновляем существующее судно
|
||||
existingVessel.updatePosition(
|
||||
vessel.getLatitude(),
|
||||
vessel.getLongitude(),
|
||||
vessel.getCourse(),
|
||||
vessel.getSpeed()
|
||||
);
|
||||
|
||||
if (mapInterface != null) {
|
||||
// Используем Handler для выполнения в главном потоке
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
try {
|
||||
mapInterface.updateAISVesselPosition(existingVessel);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка обновления позиции AIS судна на карте: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Добавляем новое судно
|
||||
aisVessels.add(vessel);
|
||||
|
||||
if (mapInterface != null) {
|
||||
// Используем Handler для выполнения в главном потоке
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
try {
|
||||
mapInterface.addAISVesselMarker(vessel);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка добавления AIS судна на карту: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем компас с ближайшими судами
|
||||
updateCompass();
|
||||
|
||||
Log.i(TAG, "AIS судно обновлено: " + vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParseError(String error) {
|
||||
Log.e(TAG, "Ошибка парсинга NMEA: " + error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет компас с текущим азимутом и ближайшими судами
|
||||
*/
|
||||
private void updateCompass() {
|
||||
if (uiUpdateCallback instanceof ExtendedUIUpdateCallback) {
|
||||
float azimuth = (float) ownVessel.getCourse();
|
||||
List<AISVessel> nearbyVessels = getNearbyVessels();
|
||||
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
((ExtendedUIUpdateCallback) uiUpdateCallback).onUpdateCompass(azimuth, nearbyVessels);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список ближайших судов (в пределах 10 км)
|
||||
*/
|
||||
private List<AISVessel> getNearbyVessels() {
|
||||
List<AISVessel> nearby = new ArrayList<>();
|
||||
double maxDistance = 10000; // 10 км в метрах
|
||||
|
||||
for (AISVessel vessel : aisVessels) {
|
||||
double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel);
|
||||
if (distance <= maxDistance) {
|
||||
nearby.add(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
return nearby;
|
||||
}
|
||||
|
||||
// Реализация UDPListenerCallback
|
||||
|
||||
@Override
|
||||
public void onDataReceived(String data, String sourceAddress, int sourcePort) {
|
||||
Log.d(TAG, "UDP данные получены от " + sourceAddress + ":" + sourcePort);
|
||||
|
||||
// Парсим полученные данные как NMEA
|
||||
nmeaParser.parseNMEA(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUDPError(String error) {
|
||||
Log.e(TAG, "UDP ошибка: " + error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "GPS Location ошибка: " + error);
|
||||
}
|
||||
|
||||
// Реализация NMEAMessageCallback
|
||||
|
||||
@Override
|
||||
public void onNMEAMessage(String message, long timestamp) {
|
||||
Log.i(TAG, "📱 Android NMEA сообщение получено в AppController: " + message);
|
||||
|
||||
// Парсим полученные данные как NMEA
|
||||
nmeaParser.parseNMEA(message);
|
||||
}
|
||||
|
||||
// Реализация MarkerClickListener
|
||||
|
||||
@Override
|
||||
public void onOwnVesselClick(Vessel vessel) {
|
||||
Log.i(TAG, "Клик по нашему судну: " + vessel);
|
||||
// Уведомляем UI о необходимости показать BottomSheet
|
||||
if (uiUpdateCallback != null) {
|
||||
Log.i(TAG, "uiUpdateCallback найден, обновляем данные судна");
|
||||
// Обновляем данные судна перед показом
|
||||
updateOwnVesselData(vessel);
|
||||
// Вызываем специальный callback для показа BottomSheet
|
||||
if (uiUpdateCallback instanceof ExtendedUIUpdateCallback) {
|
||||
Log.i(TAG, "Вызываем onShowOwnVesselBottomSheet");
|
||||
((ExtendedUIUpdateCallback) uiUpdateCallback).onShowOwnVesselBottomSheet();
|
||||
} else {
|
||||
Log.w(TAG, "uiUpdateCallback не является ExtendedUIUpdateCallback");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "uiUpdateCallback == null!");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAISVesselClick(AISVessel vessel) {
|
||||
Log.i(TAG, "Клик по AIS судну: " + vessel);
|
||||
// Уведомляем UI о необходимости показать информацию об AIS судне
|
||||
if (uiUpdateCallback != null && uiUpdateCallback instanceof ExtendedUIUpdateCallback) {
|
||||
((ExtendedUIUpdateCallback) uiUpdateCallback).onShowAISVesselInfo(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Находит AIS судно по MMSI
|
||||
*/
|
||||
private AISVessel findAISVesselByMMSI(String mmsi) {
|
||||
for (AISVessel vessel : aisVessels) {
|
||||
if (mmsi.equals(vessel.getMmsi())) {
|
||||
return vessel;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает наше судно
|
||||
*/
|
||||
public Vessel getOwnVessel() {
|
||||
return ownVessel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает список AIS судов
|
||||
*/
|
||||
public List<AISVessel> getAISVessels() {
|
||||
return new ArrayList<>(aisVessels);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все AIS суда
|
||||
*/
|
||||
public void clearAISVessels() {
|
||||
aisVessels.clear();
|
||||
if (mapInterface != null) {
|
||||
// Используем Handler для выполнения в главном потоке
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
try {
|
||||
mapInterface.clearAISVesselMarkers();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка очистки AIS судов на карте: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Центрирует карту на позиции нашего судна
|
||||
*/
|
||||
public void centerOnOwnVessel() {
|
||||
if (mapInterface != null && ownVessel != null) {
|
||||
// Используем Handler для выполнения в главном потоке
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
try {
|
||||
mapInterface.centerOnPosition(ownVessel.getLatitude(), ownVessel.getLongitude());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка центрирования карты: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы
|
||||
*/
|
||||
public void cleanup() {
|
||||
stopAllListeners();
|
||||
|
||||
if (udpListener != null) {
|
||||
udpListener.cleanup();
|
||||
}
|
||||
|
||||
if (androidNmeaListener != null) {
|
||||
androidNmeaListener.cleanup();
|
||||
}
|
||||
|
||||
if (gpsLocationListener != null) {
|
||||
gpsLocationListener.cleanup();
|
||||
}
|
||||
|
||||
if (executor != null && !executor.isShutdown()) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
|
||||
/**
|
||||
* Тестовый класс для демонстрации работы гибридного GPS подхода
|
||||
* Показывает, как координаты получаются через Location API, а остальное через NMEA
|
||||
*/
|
||||
public class GPSHybridTest {
|
||||
|
||||
private static final String TAG = "GPSHybridTest";
|
||||
|
||||
private Context context;
|
||||
private GPSLocationListener gpsLocationListener;
|
||||
private NMEAParser nmeaParser;
|
||||
private Vessel testVessel;
|
||||
|
||||
public GPSHybridTest(Context context) {
|
||||
this.context = context;
|
||||
this.testVessel = new Vessel();
|
||||
|
||||
// Инициализируем компоненты
|
||||
gpsLocationListener = new GPSLocationListener(context);
|
||||
nmeaParser = new NMEAParser();
|
||||
|
||||
// Связываем их для гибридного режима
|
||||
nmeaParser.setGPSLocationListener(gpsLocationListener);
|
||||
nmeaParser.setHybridMode(true);
|
||||
|
||||
// Устанавливаем callback'и
|
||||
gpsLocationListener.setCallback(new GPSLocationListener.LocationCallback() {
|
||||
@Override
|
||||
public void onLocationUpdated(Vessel vessel) {
|
||||
Log.i(TAG, "📍 GPS Location получен: " + vessel.getLatitude() + ", " + vessel.getLongitude());
|
||||
Log.i(TAG, "📍 Точность: " + vessel.getAccuracy() + "м");
|
||||
|
||||
// Обновляем координаты
|
||||
testVessel.setLatitude(vessel.getLatitude());
|
||||
testVessel.setLongitude(vessel.getLongitude());
|
||||
testVessel.setAccuracy(vessel.getAccuracy());
|
||||
testVessel.setFixTime(vessel.getFixTime());
|
||||
testVessel.setFixQuality(vessel.getFixQuality());
|
||||
|
||||
logVesselStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGPSStatusChanged(int status) {
|
||||
Log.i(TAG, "GPS статус: " + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(String error) {
|
||||
Log.e(TAG, "GPS Location ошибка: " + error);
|
||||
}
|
||||
});
|
||||
|
||||
nmeaParser.setListener(new NMEAParser.NMEAParserListener() {
|
||||
@Override
|
||||
public void onVesselUpdated(Vessel vessel) {
|
||||
Log.i(TAG, "📡 NMEA данные получены: course=" + vessel.getCourse() +
|
||||
", speed=" + vessel.getSpeed() +
|
||||
", satellites=" + vessel.getSatellites());
|
||||
|
||||
// Обновляем дополнительные данные
|
||||
if (vessel.getCourse() > 0) testVessel.setCourse(vessel.getCourse());
|
||||
if (vessel.getSpeed() > 0) testVessel.setSpeed(vessel.getSpeed());
|
||||
if (vessel.getSatellites() > 0) testVessel.setSatellites(vessel.getSatellites());
|
||||
if (vessel.getAltitude() != 0) testVessel.setAltitude(vessel.getAltitude());
|
||||
|
||||
logVesselStatus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAISVesselUpdated(com.grigowashere.aismap.models.AISVessel vessel) {
|
||||
Log.i(TAG, "🚢 AIS судно: " + vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParseError(String error) {
|
||||
Log.e(TAG, "NMEA ошибка: " + error);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDOPUpdated(double pdop, double hdop, double vdop) {
|
||||
Log.i(TAG, "📊 DOP: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
|
||||
|
||||
testVessel.setPdop(pdop);
|
||||
testVessel.setHdop(hdop);
|
||||
testVessel.setVdop(vdop);
|
||||
|
||||
logVesselStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает тест
|
||||
*/
|
||||
public void startTest() {
|
||||
Log.i(TAG, "🚀 Запускаем тест гибридного GPS подхода...");
|
||||
|
||||
// Запускаем GPS Location Listener
|
||||
boolean gpsSuccess = gpsLocationListener.startListening();
|
||||
if (gpsSuccess) {
|
||||
Log.i(TAG, "✅ GPS Location Listener запущен");
|
||||
} else {
|
||||
Log.e(TAG, "❌ Не удалось запустить GPS Location Listener");
|
||||
}
|
||||
|
||||
// Тестируем NMEA парсер с тестовыми сообщениями
|
||||
testNMEAParser();
|
||||
}
|
||||
|
||||
/**
|
||||
* Тестирует NMEA парсер
|
||||
*/
|
||||
private void testNMEAParser() {
|
||||
Log.i(TAG, "🧪 Тестируем NMEA парсер...");
|
||||
|
||||
// Тестовые NMEA сообщения
|
||||
String[] testMessages = {
|
||||
// GGA - количество спутников и высота
|
||||
"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
|
||||
|
||||
// RMC - курс и скорость
|
||||
"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A",
|
||||
|
||||
// VTG - курс и скорость (альтернативный источник)
|
||||
"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48",
|
||||
|
||||
// GSA - DOP и активные спутники
|
||||
"$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.8,1.0*3E",
|
||||
|
||||
// GSV - спутники в поле зрения
|
||||
"$GPGSV,3,1,12,01,05,040,3,02,46,000,4,03,42,350,4,04,42,000,4*7F"
|
||||
};
|
||||
|
||||
for (String message : testMessages) {
|
||||
Log.i(TAG, "📡 Тестируем NMEA: " + message);
|
||||
nmeaParser.parseNMEA(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Логирует текущий статус судна
|
||||
*/
|
||||
private void logVesselStatus() {
|
||||
Log.i(TAG, "🚢 === СТАТУС СУДНА ===");
|
||||
Log.i(TAG, "📍 Координаты: " + testVessel.getLatitude() + ", " + testVessel.getLongitude());
|
||||
Log.i(TAG, "🎯 Точность: " + testVessel.getAccuracy() + "м (" + testVessel.getGPSQualityDescription() + ")");
|
||||
Log.i(TAG, "🧭 Курс: " + testVessel.getCourse() + "°");
|
||||
Log.i(TAG, "⚡ Скорость: " + testVessel.getSpeed() + " узлов");
|
||||
Log.i(TAG, "Спутники: " + testVessel.getSatellites() + "/" + testVessel.getActiveSatellites());
|
||||
Log.i(TAG, "📊 DOP: PDOP=" + testVessel.getPdop() + ", HDOP=" + testVessel.getHdop() + ", VDOP=" + testVessel.getVdop());
|
||||
Log.i(TAG, "🏔️ Высота: " + testVessel.getAltitude() + "м");
|
||||
Log.i(TAG, "🔧 Качество фикса: " + testVessel.getFixQuality());
|
||||
Log.i(TAG, "==========================");
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает тест
|
||||
*/
|
||||
public void stopTest() {
|
||||
Log.i(TAG, "⏹️ Останавливаем тест...");
|
||||
|
||||
if (gpsLocationListener != null) {
|
||||
gpsLocationListener.stopListening();
|
||||
}
|
||||
|
||||
Log.i(TAG, "✅ Тест остановлен");
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (gpsLocationListener != null) {
|
||||
gpsLocationListener.cleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
import android.location.LocationListener;
|
||||
import android.location.LocationManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.location.GnssStatus;
|
||||
import android.location.GpsStatus;
|
||||
import android.location.GpsSatellite;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
|
||||
/**
|
||||
* Слушатель GPS координат через стандартный Android Location API
|
||||
* Более надежен чем NMEA для получения позиции
|
||||
*/
|
||||
public class GPSLocationListener implements LocationListener {
|
||||
|
||||
private static final String TAG = "GPSLocationListener";
|
||||
|
||||
private Context context;
|
||||
private LocationManager locationManager;
|
||||
private LocationCallback callback;
|
||||
private boolean isListening;
|
||||
|
||||
// GPS статус
|
||||
private int satelliteCount;
|
||||
private int activeSatellites;
|
||||
private double pdop = -1.0;
|
||||
private double hdop = -1.0;
|
||||
private double vdop = -1.0;
|
||||
|
||||
// Callback интерфейс
|
||||
public interface LocationCallback {
|
||||
void onLocationUpdated(Vessel vessel);
|
||||
void onGPSStatusChanged(int status);
|
||||
void onError(String error);
|
||||
}
|
||||
|
||||
public GPSLocationListener(Context context) {
|
||||
this.context = context;
|
||||
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
this.isListening = false;
|
||||
}
|
||||
|
||||
public void setCallback(LocationCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает прослушивание GPS координат
|
||||
*/
|
||||
public boolean startListening() {
|
||||
if (isListening) {
|
||||
Log.w(TAG, "GPS слушатель уже запущен");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (locationManager == null) {
|
||||
Log.e(TAG, "LocationManager недоступен");
|
||||
if (callback != null) {
|
||||
callback.onError("LocationManager недоступен");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем GPS провайдер
|
||||
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
|
||||
Log.w(TAG, "GPS провайдер отключен");
|
||||
if (callback != null) {
|
||||
callback.onError("GPS провайдер отключен");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем разрешения
|
||||
if (context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
|
||||
Log.e(TAG, "Нет разрешения ACCESS_FINE_LOCATION");
|
||||
if (callback != null) {
|
||||
callback.onError("Нет разрешения ACCESS_FINE_LOCATION");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Log.i(TAG, "=== ЗАПУСК GPS LOCATION LISTENER ===");
|
||||
|
||||
// Регистрируем слушатель локации с минимальным интервалом
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
100L, // минимальный интервал в мс
|
||||
0.0f, // минимальное расстояние в метрах
|
||||
this,
|
||||
Looper.getMainLooper()
|
||||
);
|
||||
|
||||
Log.i(TAG, "✅ GPS location updates запрошены");
|
||||
|
||||
// Регистрируем GNSS статус callback для получения информации о спутниках
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
GnssStatus.Callback gnssCallback = new GnssStatus.Callback() {
|
||||
@Override
|
||||
public void onSatelliteStatusChanged(GnssStatus status) {
|
||||
updateSatelliteInfo(status);
|
||||
}
|
||||
};
|
||||
locationManager.registerGnssStatusCallback(gnssCallback, new android.os.Handler(Looper.getMainLooper()));
|
||||
Log.i(TAG, "✅ GNSS статус callback зарегистрирован");
|
||||
}
|
||||
|
||||
// Для старых версий Android
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
GpsStatus.Listener gpsListener = new GpsStatus.Listener() {
|
||||
@Override
|
||||
public void onGpsStatusChanged(int event) {
|
||||
if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
|
||||
GpsStatus gpsStatus = locationManager.getGpsStatus(null);
|
||||
if (gpsStatus != null) {
|
||||
updateSatelliteInfoLegacy(gpsStatus);
|
||||
}
|
||||
}
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(event);
|
||||
}
|
||||
}
|
||||
};
|
||||
locationManager.addGpsStatusListener(gpsListener);
|
||||
Log.i(TAG, "✅ GPS статус слушатель зарегистрирован (старый API)");
|
||||
}
|
||||
|
||||
isListening = true;
|
||||
Log.i(TAG, "🎉 GPS Location Listener успешно запущен!");
|
||||
return true;
|
||||
|
||||
} catch (SecurityException e) {
|
||||
Log.e(TAG, "❌ Нет разрешений для доступа к GPS: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onError("Нет разрешений для доступа к GPS");
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "❌ Ошибка при запуске GPS слушателя: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
if (callback != null) {
|
||||
callback.onError("Ошибка запуска: " + e.getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает прослушивание
|
||||
*/
|
||||
public void stopListening() {
|
||||
if (!isListening) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (locationManager != null) {
|
||||
locationManager.removeUpdates(this);
|
||||
Log.i(TAG, "GPS location updates остановлены");
|
||||
}
|
||||
|
||||
isListening = false;
|
||||
Log.i(TAG, "GPS Location Listener остановлен");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при остановке GPS слушателя: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет информацию о спутниках (новый API)
|
||||
*/
|
||||
private void updateSatelliteInfo(GnssStatus status) {
|
||||
int totalCount = status.getSatelliteCount();
|
||||
int usedCount = 0;
|
||||
|
||||
for (int i = 0; i < totalCount; i++) {
|
||||
if (status.usedInFix(i)) {
|
||||
usedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
satelliteCount = totalCount;
|
||||
activeSatellites = usedCount;
|
||||
|
||||
Log.d(TAG, "Спутники: " + usedCount + "/" + totalCount + " активны");
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет информацию о спутниках (старый API)
|
||||
*/
|
||||
private void updateSatelliteInfoLegacy(GpsStatus status) {
|
||||
int totalCount = 0;
|
||||
int usedCount = 0;
|
||||
|
||||
for (GpsSatellite satellite : status.getSatellites()) {
|
||||
totalCount++;
|
||||
if (satellite.usedInFix()) {
|
||||
usedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
satelliteCount = totalCount;
|
||||
activeSatellites = usedCount;
|
||||
|
||||
Log.d(TAG, "Спутники (legacy): " + usedCount + "/" + totalCount + " активны");
|
||||
}
|
||||
|
||||
// Реализация LocationListener
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
if (location == null) {
|
||||
Log.w(TAG, "⚠️ Получен null location");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(TAG, "📍 Location обновлен: " + location.getLatitude() + ", " + location.getLongitude());
|
||||
Log.i(TAG, "📍 Точность: " + location.getAccuracy() + "м, время: " + location.getTime());
|
||||
|
||||
// Создаем объект судна с полученными данными
|
||||
Vessel vessel = new Vessel();
|
||||
vessel.setLatitude(location.getLatitude());
|
||||
vessel.setLongitude(location.getLongitude());
|
||||
vessel.setAccuracy(location.getAccuracy());
|
||||
vessel.setFixTime(location.getTime());
|
||||
|
||||
// Определяем качество фикса
|
||||
if (location.hasAccuracy()) {
|
||||
if (location.getAccuracy() <= 3) {
|
||||
vessel.setFixQuality("HIGH_ACCURACY");
|
||||
} else if (location.getAccuracy() <= 10) {
|
||||
vessel.setFixQuality("GPS");
|
||||
} else {
|
||||
vessel.setFixQuality("LOW_ACCURACY");
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем информацию о спутниках
|
||||
vessel.updateGPSQuality(satelliteCount, activeSatellites, pdop, hdop, vdop, location.getAccuracy());
|
||||
|
||||
// Отправляем обновление через callback
|
||||
if (callback != null) {
|
||||
callback.onLocationUpdated(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusChanged(String provider, int status, Bundle extras) {
|
||||
String statusName = getLocationStatusName(status);
|
||||
Log.d(TAG, "📍 Location статус изменился: " + provider + " = " + status + " (" + statusName + ")");
|
||||
|
||||
if (callback != null) {
|
||||
callback.onGPSStatusChanged(status);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderEnabled(String provider) {
|
||||
Log.i(TAG, "📍 Location провайдер включен: " + provider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProviderDisabled(String provider) {
|
||||
Log.w(TAG, "📍 Location провайдер отключен: " + provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает DOP значения (получаются из NMEA GSA)
|
||||
*/
|
||||
public void setDOPValues(double pdop, double hdop, double vdop) {
|
||||
this.pdop = pdop;
|
||||
this.hdop = hdop;
|
||||
this.vdop = vdop;
|
||||
Log.d(TAG, "📊 DOP обновлен: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает количество спутников в объект Vessel
|
||||
*/
|
||||
public void setSatellitesInVessel(Vessel vessel) {
|
||||
if (vessel != null) {
|
||||
// НЕ перезаписываем общее количество спутников из NMEA
|
||||
// vessel.setSatellites(satelliteCount); // Убираем эту строку!
|
||||
|
||||
// Устанавливаем только количество активных спутников
|
||||
vessel.setActiveSatellites(activeSatellites);
|
||||
|
||||
Log.d(TAG, "Обновлен Vessel: активных спутников=" + activeSatellites +
|
||||
" (общее количество из NMEA: " + vessel.getSatellites() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущее количество спутников
|
||||
*/
|
||||
public int getSatelliteCount() {
|
||||
return satelliteCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает количество активных спутников
|
||||
*/
|
||||
public int getActiveSatellites() {
|
||||
return activeSatellites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает детальную информацию о спутниках
|
||||
*/
|
||||
public String getSatelliteInfo() {
|
||||
return String.format("Всего: %d, Активных: %d", satelliteCount, activeSatellites);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, запущен ли слушатель
|
||||
*/
|
||||
public boolean isListening() {
|
||||
return isListening;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает название статуса локации
|
||||
*/
|
||||
private String getLocationStatusName(int status) {
|
||||
switch (status) {
|
||||
case android.location.LocationProvider.AVAILABLE: return "AVAILABLE";
|
||||
case android.location.LocationProvider.TEMPORARILY_UNAVAILABLE: return "TEMPORARILY_UNAVAILABLE";
|
||||
case android.location.LocationProvider.OUT_OF_SERVICE: return "OUT_OF_SERVICE";
|
||||
default: return "UNKNOWN(" + status + ")";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (isListening) {
|
||||
stopListening();
|
||||
}
|
||||
locationManager = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.grigowashere.aismap.maps.MapInterface;
|
||||
import com.grigowashere.aismap.maps.YandexMapImpl;
|
||||
import com.yandex.mapkit.mapview.MapView;
|
||||
|
||||
/**
|
||||
* Контроллер для управления картами
|
||||
* Инкапсулирует логику инициализации и управления различными картами
|
||||
*/
|
||||
public class MapController {
|
||||
|
||||
private static final String TAG = "MapController";
|
||||
private static boolean isYandexMapsInitialized = false;
|
||||
|
||||
private Context context;
|
||||
private MapInterface currentMapInterface;
|
||||
private MapView mapView;
|
||||
|
||||
public MapController(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует карту указанного типа
|
||||
*/
|
||||
public MapInterface initializeMap(String mapType, MapView mapView) {
|
||||
this.mapView = mapView;
|
||||
|
||||
switch (mapType.toLowerCase()) {
|
||||
case "yandex":
|
||||
return initializeYandexMaps();
|
||||
case "mapforge":
|
||||
return initializeMapForge();
|
||||
default:
|
||||
Log.e(TAG, "Неизвестный тип карты: " + mapType);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует Яндекс.Карты
|
||||
*/
|
||||
private MapInterface initializeYandexMaps() {
|
||||
try {
|
||||
// Проверяем, что Яндекс.Карты уже инициализированы
|
||||
if (!isYandexMapsInitialized) {
|
||||
Log.w(TAG, "Яндекс.Карты не инициализированы. Должны быть инициализированы в MainActivity");
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.i(TAG, "Создаем интерфейс для Яндекс.Карт");
|
||||
|
||||
// Создаем интерфейс для Яндекс.Карт
|
||||
currentMapInterface = new YandexMapImpl(context, mapView);
|
||||
return currentMapInterface;
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при создании интерфейса Яндекс.Карт: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует MapForge
|
||||
*/
|
||||
private MapInterface initializeMapForge() {
|
||||
try {
|
||||
// TODO: Реализовать инициализацию MapForge
|
||||
Log.i(TAG, "MapForge инициализация (пока не реализована)");
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при инициализации MapForge: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает карту
|
||||
*/
|
||||
public void startMap() {
|
||||
if (mapView != null) {
|
||||
mapView.onStart();
|
||||
}
|
||||
|
||||
if (isYandexMapsInitialized) {
|
||||
com.yandex.mapkit.MapKitFactory.getInstance().onStart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает карту
|
||||
*/
|
||||
public void stopMap() {
|
||||
if (mapView != null) {
|
||||
mapView.onStop();
|
||||
}
|
||||
|
||||
if (isYandexMapsInitialized) {
|
||||
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущий интерфейс карты
|
||||
*/
|
||||
public MapInterface getCurrentMapInterface() {
|
||||
return currentMapInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает флаг инициализации Яндекс.Карт
|
||||
*/
|
||||
public static void setYandexMapsInitialized(boolean initialized) {
|
||||
isYandexMapsInitialized = initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (currentMapInterface != null) {
|
||||
currentMapInterface.cleanup();
|
||||
}
|
||||
|
||||
if (mapView != null) {
|
||||
mapView.onStop();
|
||||
}
|
||||
|
||||
if (isYandexMapsInitialized) {
|
||||
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,168 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.util.Log;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.InetAddress;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Контроллер для прослушивания UDP портов
|
||||
*/
|
||||
public class UDPListener {
|
||||
|
||||
private static final String TAG = "UDPListener";
|
||||
private static final int BUFFER_SIZE = 1024;
|
||||
|
||||
private int port;
|
||||
private DatagramSocket socket;
|
||||
private ExecutorService executor;
|
||||
private AtomicBoolean isRunning;
|
||||
private UDPListenerCallback callback;
|
||||
|
||||
public interface UDPListenerCallback {
|
||||
void onDataReceived(String data, String sourceAddress, int sourcePort);
|
||||
void onUDPError(String error);
|
||||
}
|
||||
|
||||
public UDPListener(int port) {
|
||||
this.port = port;
|
||||
this.executor = Executors.newSingleThreadExecutor();
|
||||
this.isRunning = new AtomicBoolean(false);
|
||||
}
|
||||
|
||||
public void setCallback(UDPListenerCallback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает прослушивание UDP порта
|
||||
*/
|
||||
public void start() {
|
||||
if (isRunning.get()) {
|
||||
Log.w(TAG, "UDP слушатель уже запущен");
|
||||
return;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
socket = new DatagramSocket(port);
|
||||
isRunning.set(true);
|
||||
Log.i(TAG, "UDP слушатель запущен на порту " + port);
|
||||
|
||||
while (isRunning.get()) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
|
||||
|
||||
try {
|
||||
socket.receive(packet);
|
||||
String data = new String(packet.getData(), 0, packet.getLength());
|
||||
String sourceAddress = packet.getAddress().getHostAddress();
|
||||
int sourcePort = packet.getPort();
|
||||
|
||||
if (callback != null) {
|
||||
callback.onDataReceived(data, sourceAddress, sourcePort);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Получены данные от " + sourceAddress + ":" + sourcePort + ": " + data);
|
||||
|
||||
} catch (IOException e) {
|
||||
if (isRunning.get()) {
|
||||
Log.e(TAG, "Ошибка при получении UDP пакета: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onUDPError("Ошибка UDP: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Ошибка при создании UDP сокета: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onUDPError("Не удалось создать UDP сокет: " + e.getMessage());
|
||||
}
|
||||
} finally {
|
||||
stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает прослушивание UDP порта
|
||||
*/
|
||||
public void stop() {
|
||||
isRunning.set(false);
|
||||
|
||||
if (socket != null && !socket.isClosed()) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
|
||||
Log.i(TAG, "UDP слушатель остановлен");
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет UDP пакет
|
||||
*/
|
||||
public void sendData(String data, String targetAddress, int targetPort) {
|
||||
if (socket == null || socket.isClosed()) {
|
||||
Log.w(TAG, "UDP сокет не создан");
|
||||
return;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
byte[] buffer = data.getBytes();
|
||||
InetAddress address = InetAddress.getByName(targetAddress);
|
||||
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, targetPort);
|
||||
|
||||
socket.send(packet);
|
||||
Log.d(TAG, "Отправлены данные на " + targetAddress + ":" + targetPort + ": " + data);
|
||||
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Ошибка при отправке UDP пакета: " + e.getMessage());
|
||||
if (callback != null) {
|
||||
callback.onUDPError("Ошибка отправки UDP: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, запущен ли слушатель
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return isRunning.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущий порт
|
||||
*/
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает новый порт
|
||||
*/
|
||||
public void setPort(int port) {
|
||||
if (isRunning.get()) {
|
||||
Log.w(TAG, "Нельзя изменить порт во время работы");
|
||||
return;
|
||||
}
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы
|
||||
*/
|
||||
public void cleanup() {
|
||||
stop();
|
||||
if (executor != null && !executor.isShutdown()) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import org.mapsforge.core.model.LatLong;
|
||||
import org.mapsforge.map.android.view.MapView;
|
||||
import org.mapsforge.map.layer.Layers;
|
||||
import org.mapsforge.map.layer.overlay.Marker;
|
||||
import org.mapsforge.map.model.Model;
|
||||
import org.mapsforge.core.graphics.Bitmap;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Реализация карты для MapForge
|
||||
*/
|
||||
public class MapForgeImpl implements MapInterface {
|
||||
|
||||
private Context context;
|
||||
private MapView mapView;
|
||||
private Layers layers;
|
||||
private MarkerClickListener markerClickListener;
|
||||
|
||||
private Map<String, Marker> aisMarkers;
|
||||
private Marker ownVesselMarker;
|
||||
|
||||
public MapForgeImpl(Context context, MapView mapView) {
|
||||
this.context = context;
|
||||
this.mapView = mapView;
|
||||
this.aisMarkers = new HashMap<>();
|
||||
this.layers = mapView.getLayerManager().getLayers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// MapForge уже инициализирован
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (mapView != null) {
|
||||
mapView.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOwnVesselMarker(Vessel vessel) {
|
||||
if (ownVesselMarker != null) {
|
||||
layers.remove(ownVesselMarker);
|
||||
}
|
||||
|
||||
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.BLUE, vessel.getCourse());
|
||||
|
||||
ownVesselMarker = new Marker(position, icon, 0, 0);
|
||||
// MapForge не поддерживает OnTapListener напрямую, нужно использовать другой подход
|
||||
|
||||
layers.add(ownVesselMarker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateOwnVesselPosition(Vessel vessel) {
|
||||
if (ownVesselMarker != null) {
|
||||
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||
ownVesselMarker.setLatLong(newPosition);
|
||||
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.BLUE, vessel.getCourse());
|
||||
ownVesselMarker.setBitmap(icon);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAISVesselMarker(AISVessel vessel) {
|
||||
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
|
||||
|
||||
Marker marker = new Marker(position, icon, 0, 0);
|
||||
// MapForge не поддерживает OnTapListener напрямую
|
||||
|
||||
layers.add(marker);
|
||||
aisMarkers.put(vessel.getMmsi(), marker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAISVesselPosition(AISVessel vessel) {
|
||||
Marker marker = aisMarkers.get(vessel.getMmsi());
|
||||
if (marker != null) {
|
||||
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||
marker.setLatLong(newPosition);
|
||||
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
|
||||
marker.setBitmap(icon);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAISVesselMarker(String mmsi) {
|
||||
Marker marker = aisMarkers.remove(mmsi);
|
||||
if (marker != null) {
|
||||
layers.remove(marker);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAISVesselMarkers() {
|
||||
for (Marker marker : aisMarkers.values()) {
|
||||
layers.remove(marker);
|
||||
}
|
||||
aisMarkers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void centerOnPosition(double latitude, double longitude) {
|
||||
LatLong position = new LatLong(latitude, longitude);
|
||||
mapView.getModel().mapViewPosition.setCenter(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoom(float zoom) {
|
||||
mapView.getModel().mapViewPosition.setZoomLevel((byte) zoom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getZoom() {
|
||||
return mapView.getModel().mapViewPosition.getZoomLevel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLayer(String layerId, Object layerData) {
|
||||
// Реализация добавления слоев для MapForge
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLayer(String layerId) {
|
||||
// Реализация удаления слоев
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkerClickListener(MarkerClickListener listener) {
|
||||
this.markerClickListener = listener;
|
||||
}
|
||||
|
||||
private org.mapsforge.core.graphics.Bitmap createMapForgeIcon(int color, double course) {
|
||||
// Создаем простую иконку для MapForge
|
||||
// В реальном приложении нужно конвертировать Android Bitmap в MapForge Bitmap
|
||||
// Пока возвращаем null - это заглушка
|
||||
return null;
|
||||
}
|
||||
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
/**
|
||||
* Интерфейс для работы с картами
|
||||
* Позволяет использовать разные SDK карт
|
||||
*/
|
||||
public interface MapInterface {
|
||||
|
||||
/**
|
||||
* Инициализация карты
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* Очистка ресурсов карты
|
||||
*/
|
||||
void cleanup();
|
||||
|
||||
/**
|
||||
* Добавление метки нашего судна
|
||||
*/
|
||||
void addOwnVesselMarker(Vessel vessel);
|
||||
|
||||
/**
|
||||
* Обновление позиции нашего судна
|
||||
*/
|
||||
void updateOwnVesselPosition(Vessel vessel);
|
||||
|
||||
/**
|
||||
* Добавление метки AIS судна
|
||||
*/
|
||||
void addAISVesselMarker(AISVessel vessel);
|
||||
|
||||
/**
|
||||
* Обновление позиции AIS судна
|
||||
*/
|
||||
void updateAISVesselPosition(AISVessel vessel);
|
||||
|
||||
/**
|
||||
* Удаление метки AIS судна
|
||||
*/
|
||||
void removeAISVesselMarker(String mmsi);
|
||||
|
||||
/**
|
||||
* Очистка всех AIS меток
|
||||
*/
|
||||
void clearAISVesselMarkers();
|
||||
|
||||
/**
|
||||
* Центрирование карты на позиции
|
||||
*/
|
||||
void centerOnPosition(double latitude, double longitude);
|
||||
|
||||
/**
|
||||
* Установка зума карты
|
||||
*/
|
||||
void setZoom(float zoom);
|
||||
|
||||
/**
|
||||
* Получение текущего зума
|
||||
*/
|
||||
float getZoom();
|
||||
|
||||
/**
|
||||
* Добавление дополнительного слоя
|
||||
*/
|
||||
void addLayer(String layerId, Object layerData);
|
||||
|
||||
/**
|
||||
* Удаление слоя
|
||||
*/
|
||||
void removeLayer(String layerId);
|
||||
|
||||
/**
|
||||
* Установка обработчика кликов по меткам
|
||||
*/
|
||||
void setMarkerClickListener(MarkerClickListener listener);
|
||||
|
||||
/**
|
||||
* Интерфейс для обработки кликов по меткам
|
||||
*/
|
||||
interface MarkerClickListener {
|
||||
void onOwnVesselClick(Vessel vessel);
|
||||
void onAISVesselClick(AISVessel vessel);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.yandex.mapkit.Animation;
|
||||
import com.yandex.mapkit.geometry.Point;
|
||||
import com.yandex.mapkit.map.CameraPosition;
|
||||
import com.yandex.mapkit.map.MapObjectCollection;
|
||||
import com.yandex.mapkit.mapview.MapView;
|
||||
import com.yandex.runtime.image.ImageProvider;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Реализация карты для Яндекс.Карт
|
||||
*/
|
||||
public class YandexMapImpl implements MapInterface {
|
||||
|
||||
private Context context;
|
||||
private MapView mapView;
|
||||
private MapObjectCollection mapObjects;
|
||||
private MarkerClickListener markerClickListener;
|
||||
|
||||
private Map<String, com.yandex.mapkit.map.PlacemarkMapObject> aisMarkers;
|
||||
private Map<String, AISVessel> aisVessels; // Храним ссылки на AISVessel объекты
|
||||
private com.yandex.mapkit.map.PlacemarkMapObject ownVesselMarker;
|
||||
private Vessel ownVessel; // Храним ссылку на наше судно
|
||||
|
||||
// Флаги для отслеживания состояния обработчиков
|
||||
private boolean ownVesselClickListenerSet = false;
|
||||
private Map<String, Boolean> aisVesselClickListenersSet = new HashMap<>();
|
||||
|
||||
public YandexMapImpl(Context context, MapView mapView) {
|
||||
this.context = context;
|
||||
this.mapView = mapView;
|
||||
this.aisMarkers = new HashMap<>();
|
||||
this.aisVessels = new HashMap<>();
|
||||
|
||||
android.util.Log.d("YandexMapImpl", "Конструктор YandexMapImpl вызван");
|
||||
android.util.Log.d("YandexMapImpl", "Context: " + (context != null ? "установлен" : "null"));
|
||||
android.util.Log.d("YandexMapImpl", "MapView: " + (mapView != null ? "установлен" : "null"));
|
||||
|
||||
// Получение коллекции объектов карты
|
||||
try {
|
||||
this.mapObjects = mapView.getMap().getMapObjects().addCollection();
|
||||
android.util.Log.d("YandexMapImpl", "Коллекция объектов карты создана: " + (mapObjects != null ? "успешно" : "null"));
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("YandexMapImpl", "Ошибка создания коллекции объектов карты: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
android.util.Log.d("YandexMapImpl", "initialize() вызван");
|
||||
android.util.Log.d("YandexMapImpl", "mapObjects: " + (mapObjects != null ? "установлен" : "null"));
|
||||
android.util.Log.d("YandexMapImpl", "mapView: " + (mapView != null ? "установлен" : "null"));
|
||||
android.util.Log.d("YandexMapImpl", "context: " + (context != null ? "установлен" : "null"));
|
||||
|
||||
// Карта уже инициализирована в конструкторе
|
||||
if (mapObjects != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Коллекция объектов карты готова к использованию");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
if (mapObjects != null) {
|
||||
mapView.getMap().getMapObjects().remove(mapObjects);
|
||||
}
|
||||
if (mapView != null) {
|
||||
mapView.onStop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addOwnVesselMarker(Vessel vessel) {
|
||||
android.util.Log.d("YandexMapImpl", "addOwnVesselMarker вызван: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", course=" + vessel.getCourse() + "°");
|
||||
|
||||
// Сохраняем ссылку на судно
|
||||
this.ownVessel = vessel;
|
||||
|
||||
// Проверяем координаты
|
||||
if (vessel.getLatitude() == 0.0 && vessel.getLongitude() == 0.0) {
|
||||
android.util.Log.w("YandexMapImpl", "Координаты равны 0,0 - маркер не будет создан");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ownVesselMarker != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Удаляем существующий маркер");
|
||||
mapObjects.remove(ownVesselMarker);
|
||||
}
|
||||
|
||||
Point point = new Point(vessel.getLatitude(), vessel.getLongitude());
|
||||
android.util.Log.d("YandexMapImpl", "Создаем Point: " + point);
|
||||
|
||||
ownVesselMarker = mapObjects.addPlacemark(point);
|
||||
android.util.Log.d("YandexMapImpl", "Placemark создан: " + (ownVesselMarker != null ? "успешно" : "null"));
|
||||
|
||||
if (ownVesselMarker == null) {
|
||||
android.util.Log.e("YandexMapImpl", "Не удалось создать Placemark!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Используем готовую иконку стрелки с учетом курса
|
||||
android.util.Log.d("YandexMapImpl", "Устанавливаем иконку стрелки с курсом: " + vessel.getCourse() + "°");
|
||||
setMarkerIcon(ownVesselMarker, "arrowship", vessel.getCourse());
|
||||
|
||||
// Устанавливаем размер иконки
|
||||
android.util.Log.d("YandexMapImpl", "Устанавливаем IconStyle...");
|
||||
com.yandex.mapkit.map.IconStyle iconStyle = new com.yandex.mapkit.map.IconStyle();
|
||||
iconStyle.setScale(1.5f); // Увеличиваем размер иконки
|
||||
ownVesselMarker.setIconStyle(iconStyle);
|
||||
|
||||
// Устанавливаем обработчик кликов только если он еще не установлен
|
||||
if (!ownVesselClickListenerSet) {
|
||||
android.util.Log.d("YandexMapImpl", "Устанавливаем обработчик клика для маркера...");
|
||||
ownVesselMarker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
|
||||
if (markerClickListener != null && ownVessel != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
|
||||
markerClickListener.onOwnVesselClick(ownVessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
|
||||
android.util.Log.d("YandexMapImpl", "markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
|
||||
android.util.Log.d("YandexMapImpl", "ownVessel = " + (ownVessel != null ? "установлен" : "null"));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
ownVesselClickListenerSet = true;
|
||||
}
|
||||
|
||||
android.util.Log.d("YandexMapImpl", "Маркер нашего судна создан и настроен, markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
|
||||
|
||||
// Проверяем, что маркер действительно добавлен в коллекцию
|
||||
android.util.Log.d("YandexMapImpl", "Маркер добавлен в коллекцию объектов карты");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateOwnVesselPosition(Vessel vessel) {
|
||||
android.util.Log.d("YandexMapImpl", "updateOwnVesselPosition вызван: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", course=" + vessel.getCourse() + "°");
|
||||
|
||||
// Обновляем ссылку на судно
|
||||
this.ownVessel = vessel;
|
||||
|
||||
// Проверяем координаты
|
||||
if (vessel.getLatitude() == 0.0 && vessel.getLongitude() == 0.0) {
|
||||
android.util.Log.w("YandexMapImpl", "Координаты равны 0,0 - обновление пропущено");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ownVesselMarker == null) {
|
||||
// Создаем маркер нашего судна, если его еще нет
|
||||
android.util.Log.d("YandexMapImpl", "Создаем новый маркер нашего судна");
|
||||
addOwnVesselMarker(vessel);
|
||||
} else {
|
||||
// Проверяем, нужно ли обновить курс
|
||||
boolean needCourseUpdate = Math.abs(vessel.getCourse()) > 0.1; // Если курс больше 0.1 градуса
|
||||
|
||||
if (needCourseUpdate) {
|
||||
android.util.Log.d("YandexMapImpl", "Обновляем курс маркера на " + vessel.getCourse() + "°");
|
||||
// Обновляем только иконку с новым курсом
|
||||
setMarkerIcon(ownVesselMarker, "arrowship", vessel.getCourse());
|
||||
}
|
||||
|
||||
// Обновляем позицию маркера
|
||||
Point newPoint = new Point(vessel.getLatitude(), vessel.getLongitude());
|
||||
ownVesselMarker.setGeometry(newPoint);
|
||||
android.util.Log.d("YandexMapImpl", "Позиция маркера обновлена на: " + newPoint);
|
||||
|
||||
// Переустанавливаем обработчик клика после обновления маркера
|
||||
if (markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика после обновления маркера");
|
||||
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
|
||||
ownVesselMarker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
|
||||
if (markerClickListener != null && ownVessel != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
|
||||
markerClickListener.onOwnVesselClick(ownVessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
android.util.Log.d("YandexMapImpl", "Маркер нашего судна обновлен, ownVesselMarker = " + (ownVesselMarker != null ? "создан" : "null") + ", markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAISVesselMarker(AISVessel vessel) {
|
||||
android.util.Log.d("YandexMapImpl", "addAISVesselMarker вызван: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", course=" + vessel.getCourse() + "°");
|
||||
Point point = new Point(vessel.getLatitude(), vessel.getLongitude());
|
||||
com.yandex.mapkit.map.PlacemarkMapObject marker = mapObjects.addPlacemark(point);
|
||||
|
||||
// Сохраняем ссылку на судно
|
||||
aisVessels.put(vessel.getMmsi(), vessel);
|
||||
|
||||
// Используем готовую иконку стрелки для AIS судов с учетом курса
|
||||
setMarkerIcon(marker, "arrowship", vessel.getCourse());
|
||||
|
||||
// Устанавливаем размер иконки
|
||||
com.yandex.mapkit.map.IconStyle iconStyle = new com.yandex.mapkit.map.IconStyle();
|
||||
iconStyle.setScale(1.5f); // Увеличиваем размер иконки
|
||||
marker.setIconStyle(iconStyle);
|
||||
|
||||
// Установка обработчика кликов только если он еще не установлен
|
||||
String mmsi = vessel.getMmsi();
|
||||
if (!aisVesselClickListenersSet.containsKey(mmsi) || !aisVesselClickListenersSet.get(mmsi)) {
|
||||
marker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + mmsi);
|
||||
if (markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
|
||||
markerClickListener.onAISVesselClick(vessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
|
||||
android.util.Log.d("YandexMapImpl", "markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
|
||||
}
|
||||
return true;
|
||||
});
|
||||
aisVesselClickListenersSet.put(mmsi, true);
|
||||
}
|
||||
|
||||
aisMarkers.put(vessel.getMmsi(), marker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAISVesselPosition(AISVessel vessel) {
|
||||
// Обновляем ссылку на судно
|
||||
aisVessels.put(vessel.getMmsi(), vessel);
|
||||
|
||||
com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.get(vessel.getMmsi());
|
||||
if (marker != null) {
|
||||
Point newPoint = new Point(vessel.getLatitude(), vessel.getLongitude());
|
||||
marker.setGeometry(newPoint);
|
||||
|
||||
// Обновляем курс маркера, если он изменился
|
||||
if (Math.abs(vessel.getCourse()) > 0.1) {
|
||||
android.util.Log.d("YandexMapImpl", "Обновляем курс AIS маркера " + vessel.getMmsi() + " на " + vessel.getCourse() + "°");
|
||||
setMarkerIcon(marker, "arrowship", vessel.getCourse());
|
||||
}
|
||||
|
||||
// Переустанавливаем обработчик клика после обновления маркера
|
||||
if (markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика для AIS маркера: " + vessel.getMmsi());
|
||||
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
|
||||
marker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + vessel.getMmsi());
|
||||
if (markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
|
||||
markerClickListener.onAISVesselClick(vessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAISVesselMarker(String mmsi) {
|
||||
com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.remove(mmsi);
|
||||
if (marker != null) {
|
||||
mapObjects.remove(marker);
|
||||
}
|
||||
// Удаляем ссылку на судно
|
||||
aisVessels.remove(mmsi);
|
||||
// Удаляем флаг обработчика кликов
|
||||
aisVesselClickListenersSet.remove(mmsi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAISVesselMarkers() {
|
||||
for (com.yandex.mapkit.map.PlacemarkMapObject marker : aisMarkers.values()) {
|
||||
mapObjects.remove(marker);
|
||||
}
|
||||
aisMarkers.clear();
|
||||
aisVessels.clear();
|
||||
aisVesselClickListenersSet.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void centerOnPosition(double latitude, double longitude) {
|
||||
Point point = new Point(latitude, longitude);
|
||||
CameraPosition cameraPosition = new CameraPosition(point, 15.0f, 0.0f, 0.0f);
|
||||
mapView.getMap().move(cameraPosition, new Animation(Animation.Type.SMOOTH, 1.0f), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setZoom(float zoom) {
|
||||
CameraPosition currentPosition = mapView.getMap().getCameraPosition();
|
||||
Point target = currentPosition.getTarget();
|
||||
CameraPosition newPosition = new CameraPosition(target, zoom, currentPosition.getAzimuth(), currentPosition.getTilt());
|
||||
mapView.getMap().move(newPosition, new Animation(Animation.Type.SMOOTH, 0.5f), null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getZoom() {
|
||||
return mapView.getMap().getCameraPosition().getZoom();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLayer(String layerId, Object layerData) {
|
||||
// Реализация добавления дополнительных слоев
|
||||
// Зависит от конкретных требований
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeLayer(String layerId) {
|
||||
// Реализация удаления слоев
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkerClickListener(MarkerClickListener listener) {
|
||||
android.util.Log.d("YandexMapImpl", "setMarkerClickListener вызван: " + (listener != null ? "listener установлен" : "listener == null"));
|
||||
this.markerClickListener = listener;
|
||||
|
||||
// Переустанавливаем обработчики кликов для всех существующих маркеров
|
||||
updateAllMarkerClickListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет обработчики кликов для всех существующих маркеров
|
||||
* Этот метод переустанавливает обработчики для всех маркеров
|
||||
*/
|
||||
private void updateAllMarkerClickListeners() {
|
||||
android.util.Log.d("YandexMapImpl", "updateAllMarkerClickListeners вызван - переустанавливаем обработчики");
|
||||
|
||||
// Переустанавливаем обработчик для маркера нашего судна
|
||||
if (ownVesselMarker != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик для маркера нашего судна");
|
||||
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
|
||||
ownVesselMarker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
|
||||
if (markerClickListener != null && ownVessel != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
|
||||
markerClickListener.onOwnVesselClick(ownVessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
ownVesselClickListenerSet = true;
|
||||
}
|
||||
|
||||
// Переустанавливаем обработчики для AIS маркеров
|
||||
for (Map.Entry<String, com.yandex.mapkit.map.PlacemarkMapObject> entry : aisMarkers.entrySet()) {
|
||||
String mmsi = entry.getKey();
|
||||
com.yandex.mapkit.map.PlacemarkMapObject marker = entry.getValue();
|
||||
AISVessel vessel = aisVessels.get(mmsi);
|
||||
|
||||
if (marker != null && vessel != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик для AIS маркера: " + mmsi);
|
||||
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
|
||||
marker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + mmsi);
|
||||
if (markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
|
||||
markerClickListener.onAISVesselClick(vessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
aisVesselClickListenersSet.put(mmsi, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание иконки судна
|
||||
*/
|
||||
private Bitmap createVesselIcon(int color, double course) {
|
||||
try {
|
||||
int size = 64; // Увеличиваем размер для лучшей видимости
|
||||
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
Paint paint = new Paint();
|
||||
paint.setColor(color);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setStrokeWidth(3.0f);
|
||||
|
||||
// Рисуем треугольник-стрелку, направленную вверх (по умолчанию)
|
||||
android.graphics.Path path = new android.graphics.Path();
|
||||
path.moveTo(size / 2f, 0); // вершина
|
||||
path.lineTo(size * 0.1f, size * 0.8f); // левый нижний угол
|
||||
path.lineTo(size * 0.3f, size * 0.6f); // левая внутренняя точка
|
||||
path.lineTo(size * 0.3f, size * 0.9f); // левая нижняя точка
|
||||
path.lineTo(size * 0.7f, size * 0.9f); // правая нижняя точка
|
||||
path.lineTo(size * 0.7f, size * 0.6f); // правая внутренняя точка
|
||||
path.lineTo(size * 0.9f, size * 0.8f); // правый нижний угол
|
||||
path.close();
|
||||
|
||||
// Поворачиваем стрелку на курс (курс 0° = стрелка направлена вверх)
|
||||
// В морской навигации курс 0° = север, 90° = восток, 180° = юг, 270° = запад
|
||||
canvas.save();
|
||||
canvas.rotate((float) course, size / 2f, size / 2f);
|
||||
canvas.drawPath(path, paint);
|
||||
canvas.restore();
|
||||
|
||||
android.util.Log.d("YandexMapImpl", "Программная иконка с курсом " + course + "° создана успешно, размер: " + size + "x" + size);
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("YandexMapImpl", "Ошибка создания программной иконки: " + e.getMessage(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получение MapView для использования в layout
|
||||
*/
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно пересоздает маркер нашего судна с иконкой
|
||||
*/
|
||||
public void recreateOwnVesselMarker(Vessel vessel) {
|
||||
android.util.Log.d("YandexMapImpl", "Принудительно пересоздаем маркер нашего судна");
|
||||
if (ownVesselMarker != null) {
|
||||
mapObjects.remove(ownVesselMarker);
|
||||
ownVesselMarker = null;
|
||||
}
|
||||
addOwnVesselMarker(vessel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет обработчики кликов для всех маркеров
|
||||
* Вызывается после закрытия BottomSheet для восстановления функциональности
|
||||
*/
|
||||
public void refreshMarkerClickListeners() {
|
||||
android.util.Log.d("YandexMapImpl", "refreshMarkerClickListeners вызван - переустанавливаем все обработчики");
|
||||
updateAllMarkerClickListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает иконку для маркера с fallback
|
||||
*/
|
||||
private void setMarkerIcon(com.yandex.mapkit.map.PlacemarkMapObject marker, String iconName, double course) {
|
||||
try {
|
||||
android.util.Log.d("YandexMapImpl", "Пытаемся установить иконку: " + iconName + " с курсом: " + course + "°");
|
||||
android.util.Log.d("YandexMapImpl", "Package name: " + context.getPackageName());
|
||||
|
||||
// Сначала пробуем создать программную иконку с учетом курса
|
||||
android.util.Log.d("YandexMapImpl", "Создаем программную иконку стрелки с курсом " + course + "°...");
|
||||
Bitmap iconBitmap = createVesselIcon(android.graphics.Color.BLUE, course);
|
||||
if (iconBitmap != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Программная иконка с курсом " + course + "° создана, устанавливаем...");
|
||||
marker.setIcon(ImageProvider.fromBitmap(iconBitmap));
|
||||
android.util.Log.d("YandexMapImpl", "Программная иконка с курсом " + course + "° установлена успешно");
|
||||
return;
|
||||
}
|
||||
|
||||
// Если программная иконка не создалась, пробуем ресурс
|
||||
int iconResId = context.getResources().getIdentifier(iconName, "drawable", context.getPackageName());
|
||||
android.util.Log.d("YandexMapImpl", "ID ресурса " + iconName + ": " + iconResId);
|
||||
|
||||
if (iconResId != 0) {
|
||||
android.util.Log.d("YandexMapImpl", "Устанавливаем иконку из ресурса...");
|
||||
marker.setIcon(ImageProvider.fromResource(context, iconResId));
|
||||
android.util.Log.d("YandexMapImpl", "Иконка " + iconName + " установлена успешно");
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "Не удалось найти ресурс " + iconName);
|
||||
android.util.Log.d("YandexMapImpl", "Используем fallback иконку...");
|
||||
// Создаем простую иконку как fallback
|
||||
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
|
||||
android.util.Log.d("YandexMapImpl", "Fallback иконка установлена");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("YandexMapImpl", "Ошибка установки иконки " + iconName + ": " + e.getMessage(), e);
|
||||
android.util.Log.d("YandexMapImpl", "Используем fallback иконку после ошибки...");
|
||||
// Создаем простую иконку как fallback
|
||||
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
|
||||
android.util.Log.d("YandexMapImpl", "Fallback иконка установлена после ошибки");
|
||||
}
|
||||
|
||||
// После установки иконки проверяем, что обработчик клика все еще работает
|
||||
// Это может помочь с проблемами, когда установка иконки нарушает обработчики
|
||||
android.util.Log.d("YandexMapImpl", "Иконка установлена, проверяем обработчик клика...");
|
||||
|
||||
// Дополнительная проверка: если это маркер нашего судна, переустанавливаем обработчик клика
|
||||
if (marker == ownVesselMarker && markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика для маркера нашего судна после установки иконки");
|
||||
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
|
||||
marker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
|
||||
if (markerClickListener != null && ownVessel != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
|
||||
markerClickListener.onOwnVesselClick(ownVessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Дополнительная проверка: если это AIS маркер, переустанавливаем обработчик клика
|
||||
for (Map.Entry<String, com.yandex.mapkit.map.PlacemarkMapObject> entry : aisMarkers.entrySet()) {
|
||||
if (entry.getValue() == marker && markerClickListener != null) {
|
||||
String mmsi = entry.getKey();
|
||||
AISVessel vessel = aisVessels.get(mmsi);
|
||||
if (vessel != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика для AIS маркера " + mmsi + " после установки иконки");
|
||||
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
|
||||
marker.addTapListener((mapObject, point1) -> {
|
||||
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + mmsi);
|
||||
if (markerClickListener != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
|
||||
markerClickListener.onAISVesselClick(vessel);
|
||||
} else {
|
||||
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.grigowashere.aismap.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Модель AIS судна
|
||||
*/
|
||||
public class AISVessel {
|
||||
private String mmsi; // Maritime Mobile Service Identity
|
||||
private String vesselName; // название судна
|
||||
private String callSign; // позывной
|
||||
private int imo; // IMO номер
|
||||
private String vesselType; // тип судна
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private double course; // курс в градусах (0-360)
|
||||
private double speed; // скорость в узлах
|
||||
private double heading; // направление движения в градусах
|
||||
private double length; // длина судна в метрах
|
||||
private double width; // ширина судна в метрах
|
||||
private double draft; // осадка в метрах
|
||||
private String destination; // пункт назначения
|
||||
private LocalDateTime eta; // предполагаемое время прибытия
|
||||
private LocalDateTime lastUpdate;
|
||||
private int signalStrength; // сила AIS сигнала
|
||||
private boolean isActive; // активно ли судно
|
||||
private String navigationalStatus; // навигационный статус
|
||||
private String lastSafetyMessage; // последнее сообщение безопасности
|
||||
private boolean positionAccuracy; // точность позиции
|
||||
private String vesselClass; // класс судна (Class A, Class B, Extended Class B)
|
||||
private String vendorId; // идентификатор производителя оборудования
|
||||
|
||||
public AISVessel() {
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
public AISVessel(String mmsi) {
|
||||
this();
|
||||
this.mmsi = mmsi;
|
||||
}
|
||||
|
||||
// Геттеры и сеттеры
|
||||
public String getMmsi() { return mmsi; }
|
||||
public void setMmsi(String mmsi) { this.mmsi = mmsi; }
|
||||
|
||||
public String getVesselName() { return vesselName; }
|
||||
public void setVesselName(String vesselName) { this.vesselName = vesselName; }
|
||||
|
||||
public String getCallSign() { return callSign; }
|
||||
public void setCallSign(String callSign) { this.callSign = callSign; }
|
||||
|
||||
public int getImo() { return imo; }
|
||||
public void setImo(int imo) { this.imo = imo; }
|
||||
|
||||
public String getVesselType() { return vesselType; }
|
||||
public void setVesselType(String vesselType) { this.vesselType = vesselType; }
|
||||
|
||||
public double getLatitude() { return latitude; }
|
||||
public void setLatitude(double latitude) { this.latitude = latitude; }
|
||||
|
||||
public double getLongitude() { return longitude; }
|
||||
public void setLongitude(double longitude) { this.longitude = longitude; }
|
||||
|
||||
public double getCourse() { return course; }
|
||||
public void setCourse(double course) { this.course = course; }
|
||||
|
||||
public double getSpeed() { return speed; }
|
||||
public void setSpeed(double speed) { this.speed = speed; }
|
||||
|
||||
public double getHeading() { return heading; }
|
||||
public void setHeading(double heading) { this.heading = heading; }
|
||||
|
||||
public double getLength() { return length; }
|
||||
public void setLength(double length) { this.length = length; }
|
||||
|
||||
public double getWidth() { return width; }
|
||||
public void setWidth(double width) { this.width = width; }
|
||||
|
||||
public double getDraft() { return draft; }
|
||||
public void setDraft(double draft) { this.draft = draft; }
|
||||
|
||||
public String getDestination() { return destination; }
|
||||
public void setDestination(String destination) { this.destination = destination; }
|
||||
|
||||
public LocalDateTime getEta() { return eta; }
|
||||
public void setEta(LocalDateTime eta) { this.eta = eta; }
|
||||
|
||||
public LocalDateTime getLastUpdate() { return lastUpdate; }
|
||||
public void setLastUpdate(LocalDateTime lastUpdate) { this.lastUpdate = lastUpdate; }
|
||||
|
||||
public int getSignalStrength() { return signalStrength; }
|
||||
public void setSignalStrength(int signalStrength) { this.signalStrength = signalStrength; }
|
||||
|
||||
public boolean isActive() { return isActive; }
|
||||
public void setActive(boolean active) { isActive = active; }
|
||||
|
||||
public String getNavigationalStatus() { return navigationalStatus; }
|
||||
public void setNavigationalStatus(String navigationalStatus) { this.navigationalStatus = navigationalStatus; }
|
||||
|
||||
public String getLastSafetyMessage() { return lastSafetyMessage; }
|
||||
public void setLastSafetyMessage(String lastSafetyMessage) { this.lastSafetyMessage = lastSafetyMessage; }
|
||||
|
||||
public boolean isPositionAccuracy() { return positionAccuracy; }
|
||||
public void setPositionAccuracy(boolean positionAccuracy) { this.positionAccuracy = positionAccuracy; }
|
||||
|
||||
public String getVesselClass() { return vesselClass; }
|
||||
public void setVesselClass(String vesselClass) { this.vesselClass = vesselClass; }
|
||||
|
||||
public String getVendorId() { return vendorId; }
|
||||
public void setVendorId(String vendorId) { this.vendorId = vendorId; }
|
||||
|
||||
/**
|
||||
* Обновляет позицию и курс судна
|
||||
*/
|
||||
public void updatePosition(double latitude, double longitude, double course, double speed) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.course = course;
|
||||
this.speed = speed;
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, не устарели ли данные (больше 10 минут)
|
||||
*/
|
||||
public boolean isDataStale() {
|
||||
return LocalDateTime.now().minusMinutes(10).isAfter(lastUpdate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AISVessel{" +
|
||||
"mmsi='" + mmsi + '\'' +
|
||||
", name='" + vesselName + '\'' +
|
||||
", lat=" + latitude +
|
||||
", lon=" + longitude +
|
||||
", course=" + course +
|
||||
", speed=" + speed +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.grigowashere.aismap.models;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* Модель нашего судна
|
||||
*/
|
||||
public class Vessel {
|
||||
private double latitude;
|
||||
private double longitude;
|
||||
private double course; // курс в градусах (0-360)
|
||||
private double speed; // скорость в узлах
|
||||
private double heading; // направление движения в градусах
|
||||
private double magneticCompass; // магнитный компас в градусах (0-360)
|
||||
private int signalStrength; // сила сигнала GPS (0-100)
|
||||
private LocalDateTime lastUpdate;
|
||||
private String vesselName;
|
||||
private String mmsi; // Maritime Mobile Service Identity
|
||||
private String callSign; // позывной
|
||||
private double altitude; // высота над уровнем моря
|
||||
private int satellites; // общее количество спутников
|
||||
private int activeSatellites; // количество активных спутников в фиксе
|
||||
|
||||
// DOP (Dilution of Precision) - показатели качества GPS
|
||||
private double pdop; // Position DOP - общая точность позиции
|
||||
private double hdop; // Horizontal DOP - точность по горизонтали
|
||||
private double vdop; // Vertical DOP - точность по вертикали
|
||||
|
||||
// Дополнительные GPS параметры
|
||||
private float accuracy; // точность в метрах
|
||||
private long fixTime; // время последнего фикса
|
||||
private String fixQuality; // качество фикса (GPS, DGPS, RTK и т.д.)
|
||||
|
||||
public Vessel() {
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
this.fixQuality = "NO_FIX";
|
||||
this.accuracy = -1.0f;
|
||||
}
|
||||
|
||||
public Vessel(double latitude, double longitude) {
|
||||
this();
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
// Геттеры и сеттеры
|
||||
public double getLatitude() { return latitude; }
|
||||
public void setLatitude(double latitude) { this.latitude = latitude; }
|
||||
|
||||
public double getLongitude() { return longitude; }
|
||||
public void setLongitude(double longitude) { this.longitude = longitude; }
|
||||
|
||||
public double getCourse() { return course; }
|
||||
public void setCourse(double course) { this.course = course; }
|
||||
|
||||
public double getSpeed() { return speed; }
|
||||
public void setSpeed(double speed) { this.speed = speed; }
|
||||
|
||||
public double getHeading() { return heading; }
|
||||
public void setHeading(double heading) { this.heading = heading; }
|
||||
|
||||
public double getMagneticCompass() { return magneticCompass; }
|
||||
public void setMagneticCompass(double magneticCompass) { this.magneticCompass = magneticCompass; }
|
||||
|
||||
public int getSignalStrength() { return signalStrength; }
|
||||
public void setSignalStrength(int signalStrength) { this.signalStrength = signalStrength; }
|
||||
|
||||
public LocalDateTime getLastUpdate() { return lastUpdate; }
|
||||
public void setLastUpdate(LocalDateTime lastUpdate) { this.lastUpdate = lastUpdate; }
|
||||
|
||||
public String getVesselName() { return vesselName; }
|
||||
public void setVesselName(String vesselName) { this.vesselName = vesselName; }
|
||||
|
||||
public String getMmsi() { return mmsi; }
|
||||
public void setMmsi(String mmsi) { this.mmsi = mmsi; }
|
||||
|
||||
public String getCallSign() { return callSign; }
|
||||
public void setCallSign(String callSign) { this.callSign = callSign; }
|
||||
|
||||
public double getAltitude() { return altitude; }
|
||||
public void setAltitude(double altitude) { this.altitude = altitude; }
|
||||
|
||||
public int getSatellites() { return satellites; }
|
||||
public void setSatellites(int satellites) { this.satellites = satellites; }
|
||||
|
||||
public int getActiveSatellites() { return activeSatellites; }
|
||||
public void setActiveSatellites(int activeSatellites) { this.activeSatellites = activeSatellites; }
|
||||
|
||||
public double getPdop() { return pdop; }
|
||||
public void setPdop(double pdop) { this.pdop = pdop; }
|
||||
|
||||
public double getHdop() { return hdop; }
|
||||
public void setHdop(double hdop) { this.hdop = hdop; }
|
||||
|
||||
public double getVdop() { return vdop; }
|
||||
public void setVdop(double vdop) { this.vdop = vdop; }
|
||||
|
||||
public float getAccuracy() { return accuracy; }
|
||||
public void setAccuracy(float accuracy) { this.accuracy = accuracy; }
|
||||
|
||||
public long getFixTime() { return fixTime; }
|
||||
public void setFixTime(long fixTime) { this.fixTime = fixTime; }
|
||||
|
||||
public String getFixQuality() { return fixQuality; }
|
||||
public void setFixQuality(String fixQuality) { this.fixQuality = fixQuality; }
|
||||
|
||||
/**
|
||||
* Обновляет данные судна
|
||||
*/
|
||||
public void updatePosition(double latitude, double longitude, double course, double speed) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.course = course;
|
||||
this.speed = speed;
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет GPS качество
|
||||
*/
|
||||
public void updateGPSQuality(int satellites, int activeSatellites, double pdop, double hdop, double vdop, float accuracy) {
|
||||
this.satellites = satellites;
|
||||
this.activeSatellites = activeSatellites;
|
||||
this.pdop = pdop;
|
||||
this.hdop = hdop;
|
||||
this.vdop = vdop;
|
||||
this.accuracy = accuracy;
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает качество GPS сигнала в процентах
|
||||
*/
|
||||
public int getGPSQualityPercentage() {
|
||||
if (accuracy <= 0) return 0;
|
||||
if (accuracy <= 3) return 100; // Отличное качество (≤3м)
|
||||
if (accuracy <= 5) return 90; // Очень хорошее (≤5м)
|
||||
if (accuracy <= 10) return 80; // Хорошее (≤10м)
|
||||
if (accuracy <= 20) return 60; // Удовлетворительное (≤20м)
|
||||
if (accuracy <= 50) return 40; // Плохое (≤50м)
|
||||
return 20; // Очень плохое (>50м)
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текстовое описание качества GPS
|
||||
*/
|
||||
public String getGPSQualityDescription() {
|
||||
int quality = getGPSQualityPercentage();
|
||||
if (quality >= 90) return "Отличное";
|
||||
if (quality >= 80) return "Очень хорошее";
|
||||
if (quality >= 60) return "Хорошее";
|
||||
if (quality >= 40) return "Удовлетворительное";
|
||||
if (quality >= 20) return "Плохое";
|
||||
return "Очень плохое";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Vessel{" +
|
||||
"lat=" + latitude +
|
||||
", lon=" + longitude +
|
||||
", course=" + course +
|
||||
", speed=" + speed +
|
||||
", name='" + vesselName + '\'' +
|
||||
", satellites=" + satellites + "/" + activeSatellites +
|
||||
", accuracy=" + accuracy + "m" +
|
||||
", quality=" + getGPSQualityDescription() +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.grigowashere.aismap.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Класс для работы с магнитным компасом устройства
|
||||
*/
|
||||
public class CompassSensor implements SensorEventListener {
|
||||
private static final String TAG = "CompassSensor";
|
||||
|
||||
private SensorManager sensorManager;
|
||||
private Sensor accelerometer;
|
||||
private Sensor magnetometer;
|
||||
|
||||
private float[] accelerometerReading = new float[3];
|
||||
private float[] magnetometerReading = new float[3];
|
||||
|
||||
private float[] rotationMatrix = new float[9];
|
||||
private float[] orientationAngles = new float[3];
|
||||
|
||||
private CompassListener compassListener;
|
||||
private boolean isListening = false;
|
||||
|
||||
// Скользящий фильтр для сглаживания значений
|
||||
private static final int FILTER_SIZE = 60;
|
||||
private float[] azimuthBuffer = new float[FILTER_SIZE];
|
||||
private int bufferIndex = 0;
|
||||
private boolean bufferFull = false;
|
||||
|
||||
public interface CompassListener {
|
||||
void onCompassChanged(float azimuth);
|
||||
}
|
||||
|
||||
public CompassSensor(Context context) {
|
||||
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
||||
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
||||
magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
|
||||
}
|
||||
|
||||
public void startListening(CompassListener listener) {
|
||||
if (isListening) {
|
||||
stopListening();
|
||||
}
|
||||
|
||||
this.compassListener = listener;
|
||||
|
||||
if (accelerometer != null && magnetometer != null) {
|
||||
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
|
||||
sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_GAME);
|
||||
isListening = true;
|
||||
Log.d(TAG, "Compass sensor started");
|
||||
} else {
|
||||
Log.e(TAG, "Compass sensors not available");
|
||||
}
|
||||
}
|
||||
|
||||
public void stopListening() {
|
||||
if (isListening) {
|
||||
sensorManager.unregisterListener(this);
|
||||
isListening = false;
|
||||
compassListener = null;
|
||||
|
||||
// Сбрасываем фильтр
|
||||
resetFilter();
|
||||
|
||||
Log.d(TAG, "Compass sensor stopped");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сбрасывает фильтр
|
||||
*/
|
||||
private void resetFilter() {
|
||||
bufferIndex = 0;
|
||||
bufferFull = false;
|
||||
for (int i = 0; i < FILTER_SIZE; i++) {
|
||||
azimuthBuffer[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.length);
|
||||
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
|
||||
System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.length);
|
||||
}
|
||||
|
||||
// Обновляем ориентацию
|
||||
updateOrientationAngles();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// Можно добавить логику для обработки изменений точности
|
||||
}
|
||||
|
||||
private void updateOrientationAngles() {
|
||||
// Обновляем матрицу вращения
|
||||
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading);
|
||||
|
||||
// Получаем углы ориентации
|
||||
SensorManager.getOrientation(rotationMatrix, orientationAngles);
|
||||
|
||||
// Азимут (направление на север) в радианах, конвертируем в градусы
|
||||
float azimuthInRadians = orientationAngles[0];
|
||||
float azimuthInDegrees = (float) Math.toDegrees(azimuthInRadians);
|
||||
|
||||
// Нормализуем до диапазона 0-360
|
||||
if (azimuthInDegrees < 0) {
|
||||
azimuthInDegrees += 360;
|
||||
}
|
||||
|
||||
// Применяем скользящий фильтр
|
||||
float filteredAzimuth = applyLowPassFilter(azimuthInDegrees);
|
||||
|
||||
// Уведомляем слушателя
|
||||
if (compassListener != null) {
|
||||
compassListener.onCompassChanged(filteredAzimuth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Применяет скользящий фильтр для сглаживания значений
|
||||
*/
|
||||
private float applyLowPassFilter(float newValue) {
|
||||
// Добавляем новое значение в буфер
|
||||
azimuthBuffer[bufferIndex] = newValue;
|
||||
bufferIndex = (bufferIndex + 1) % FILTER_SIZE;
|
||||
|
||||
if (bufferIndex == 0) {
|
||||
bufferFull = true;
|
||||
}
|
||||
|
||||
// Вычисляем среднее значение
|
||||
float sum = 0;
|
||||
int count = bufferFull ? FILTER_SIZE : bufferIndex;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
sum += azimuthBuffer[i];
|
||||
}
|
||||
|
||||
return sum / count;
|
||||
}
|
||||
|
||||
public boolean isAvailable() {
|
||||
return accelerometer != null && magnetometer != null;
|
||||
}
|
||||
|
||||
public boolean isListening() {
|
||||
return isListening;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.grigowashere.aismap.utils;
|
||||
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
|
||||
/**
|
||||
* Утилиты для геодезических расчетов
|
||||
*/
|
||||
public class GeoUtils {
|
||||
|
||||
private static final double EARTH_RADIUS = 6371000; // радиус Земли в метрах
|
||||
|
||||
/**
|
||||
* Рассчитывает расстояние между двумя точками по формуле гаверсинуса
|
||||
* @param lat1 широта первой точки в градусах
|
||||
* @param lon1 долгота первой точки в градусах
|
||||
* @param lat2 широта второй точки в градусах
|
||||
* @param lon2 долгота второй точки в градусах
|
||||
* @return расстояние в метрах
|
||||
*/
|
||||
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double deltaLat = Math.toRadians(lat2 - lat1);
|
||||
double deltaLon = Math.toRadians(lon2 - lon1);
|
||||
|
||||
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 * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает расстояние от нашего судна до AIS судна
|
||||
* @param ourVessel наше судно
|
||||
* @param aisVessel AIS судно
|
||||
* @return расстояние в метрах
|
||||
*/
|
||||
public static double calculateDistance(Vessel ourVessel, AISVessel aisVessel) {
|
||||
return calculateDistance(
|
||||
ourVessel.getLatitude(), ourVessel.getLongitude(),
|
||||
aisVessel.getLatitude(), aisVessel.getLongitude()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает пеленг от первой точки ко второй
|
||||
* @param lat1 широта первой точки в градусах
|
||||
* @param lon1 долгота первой точки в градусах
|
||||
* @param lat2 широта второй точки в градусах
|
||||
* @param lon2 долгота второй точки в градусах
|
||||
* @return пеленг в градусах (0-360)
|
||||
*/
|
||||
public static double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double deltaLon = Math.toRadians(lon2 - lon1);
|
||||
|
||||
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 bearing = Math.toDegrees(Math.atan2(y, x));
|
||||
return (bearing + 360) % 360; // нормализуем к диапазону 0-360
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает пеленг от нашего судна до AIS судна
|
||||
* @param ourVessel наше судно
|
||||
* @param aisVessel AIS судно
|
||||
* @return пеленг в градусах (0-360)
|
||||
*/
|
||||
public static double calculateBearing(Vessel ourVessel, AISVessel aisVessel) {
|
||||
return calculateBearing(
|
||||
ourVessel.getLatitude(), ourVessel.getLongitude(),
|
||||
aisVessel.getLatitude(), aisVessel.getLongitude()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует навигационный статус в числовой код
|
||||
* @param navigationalStatus строковый статус
|
||||
* @return числовой код статуса
|
||||
*/
|
||||
public static int getNavigationStatusCode(String navigationalStatus) {
|
||||
if (navigationalStatus == null) return -1;
|
||||
|
||||
switch (navigationalStatus.toLowerCase()) {
|
||||
case "under way using engine":
|
||||
case "under way":
|
||||
return 0;
|
||||
case "at anchor":
|
||||
case "anchored":
|
||||
return 1;
|
||||
case "not under command":
|
||||
return 2;
|
||||
case "restricted manoeuvrability":
|
||||
return 3;
|
||||
case "constrained by her draught":
|
||||
return 4;
|
||||
case "moored":
|
||||
return 5;
|
||||
case "aground":
|
||||
return 6;
|
||||
case "engaged in fishing":
|
||||
return 7;
|
||||
case "under way sailing":
|
||||
return 8;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
package com.grigowashere.aismap.view;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
public abstract class BaseDockWidget extends FrameLayout {
|
||||
private static final String TAG = "BaseDockWidget";
|
||||
|
||||
// Константы
|
||||
protected static final int CIRCLE_SIZE_DP = 120;
|
||||
protected static final int DEFAULT_DOCK_HEIGHT_DP = 80;
|
||||
protected static final float MIN_SCALE = 0.5f;
|
||||
protected static final float MAX_SCALE = 2.0f;
|
||||
protected static final float SCALE_STEP = 0.1f;
|
||||
|
||||
// Состояние виджета
|
||||
protected boolean isDocked = true; // По умолчанию в dock-режиме
|
||||
protected boolean dockTop = true;
|
||||
protected boolean isMorphing = false;
|
||||
protected float morphProgress = 0.0f; // 0 = dock, 1 = circle
|
||||
|
||||
// Перетаскивание
|
||||
protected boolean dragging = false;
|
||||
protected float dX, dY;
|
||||
protected Float targetDragX = null;
|
||||
protected Float targetDragY = null;
|
||||
|
||||
// Изменение размера в dock режиме
|
||||
protected boolean resizingDock = false;
|
||||
protected float lastTouchY;
|
||||
protected int dockHeightPx = 0;
|
||||
|
||||
// Масштабирование
|
||||
protected float scaleFactor = 1.0f;
|
||||
protected float initialDistance = 0;
|
||||
protected float initialScale = 1.0f;
|
||||
|
||||
// Анимация
|
||||
protected ValueAnimator morphAnimator;
|
||||
|
||||
// Интерфейс для уведомления об изменении размера
|
||||
public interface OnDockResizeListener {
|
||||
void onDockResize(int newHeight);
|
||||
}
|
||||
protected OnDockResizeListener dockResizeListener;
|
||||
|
||||
public BaseDockWidget(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public BaseDockWidget(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setClickable(true);
|
||||
setFocusable(true);
|
||||
|
||||
// Инициализируем в dock-режиме
|
||||
post(() -> {
|
||||
if (isDocked) {
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
setX(0);
|
||||
setY(0);
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
lp.height = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
|
||||
dockHeightPx = 0; // Сбрасываем сохраненную высоту
|
||||
setLayoutParams(lp);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (isMorphing) return true;
|
||||
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
return handleTouchDown(event);
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
return handleTouchMove(event);
|
||||
case MotionEvent.ACTION_UP:
|
||||
return handleTouchUp(event);
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
return handlePointerDown(event);
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
return handlePointerUp(event);
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
private boolean handleTouchDown(MotionEvent event) {
|
||||
float x = event.getX();
|
||||
float y = event.getY();
|
||||
|
||||
if (isDocked) {
|
||||
// Проверяем зоны изменения размера в зависимости от позиции закрепления
|
||||
if (dockTop) {
|
||||
// Если закреплен сверху, зона изменения размера только снизу
|
||||
if (y > getHeight() - dp(24)) {
|
||||
resizingDock = true;
|
||||
lastTouchY = event.getRawY();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Если закреплен снизу, зона изменения размера только сверху
|
||||
if (y < dp(24)) {
|
||||
resizingDock = true;
|
||||
lastTouchY = event.getRawY();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Если нажали в центральной области dock-виджета, переводим в movable режим
|
||||
// Вычисляем новую позицию, чтобы виджет был под пальцем
|
||||
float newX = event.getRawX() - dp(CIRCLE_SIZE_DP) / 2;
|
||||
float newY = event.getRawY() - dp(CIRCLE_SIZE_DP) / 2;
|
||||
|
||||
// Ограничиваем в пределах экрана
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
newX = Math.max(0, Math.min(newX, parent.getWidth() - dp(CIRCLE_SIZE_DP)));
|
||||
newY = Math.max(0, Math.min(newY, parent.getHeight() - dp(CIRCLE_SIZE_DP)));
|
||||
}
|
||||
|
||||
setDocked(false, dockTop, newX, newY);
|
||||
}
|
||||
|
||||
// Обычное перетаскивание
|
||||
dragging = true;
|
||||
dX = getX() - event.getRawX();
|
||||
dY = getY() - event.getRawY();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handleTouchMove(MotionEvent event) {
|
||||
if (resizingDock) {
|
||||
handleDockResize(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Обработка масштабирования двумя пальцами
|
||||
if (event.getPointerCount() == 2 && initialDistance > 0) {
|
||||
float distance = getDistance(event);
|
||||
float scale = distance / initialDistance;
|
||||
scaleFactor = Math.max(MIN_SCALE, Math.min(MAX_SCALE, initialScale * scale));
|
||||
requestLayout();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
float newX = event.getRawX() + dX;
|
||||
float newY = event.getRawY() + dY;
|
||||
|
||||
// Ограничиваем движение в пределах родителя
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
if (parent != null) {
|
||||
newX = Math.max(0, Math.min(newX, parent.getWidth() - getWidth()));
|
||||
newY = Math.max(0, Math.min(newY, parent.getHeight() - getHeight()));
|
||||
}
|
||||
|
||||
setX(newX);
|
||||
setY(newY);
|
||||
|
||||
// Проверяем возможность докинга
|
||||
checkDocking(event);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleTouchUp(MotionEvent event) {
|
||||
if (resizingDock) {
|
||||
resizingDock = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (dragging) {
|
||||
dragging = false;
|
||||
|
||||
// Если виджет находится в зоне докинга, доким его
|
||||
if (shouldDock(event)) {
|
||||
performDocking(event);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handlePointerDown(MotionEvent event) {
|
||||
if (event.getPointerCount() == 2) {
|
||||
initialDistance = getDistance(event);
|
||||
initialScale = scaleFactor;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean handlePointerUp(MotionEvent event) {
|
||||
if (event.getPointerCount() < 2) {
|
||||
initialDistance = 0;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handleDockResize(MotionEvent event) {
|
||||
float deltaY = event.getRawY() - lastTouchY;
|
||||
lastTouchY = event.getRawY();
|
||||
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
int newHeight = lp.height;
|
||||
|
||||
// Направление изменения размера зависит от позиции закрепления
|
||||
if (dockTop) {
|
||||
// Если закреплен сверху, увеличиваем размер при движении вниз
|
||||
newHeight += (int) deltaY;
|
||||
} else {
|
||||
// Если закреплен снизу, увеличиваем размер при движении вверх
|
||||
newHeight -= (int) deltaY;
|
||||
}
|
||||
|
||||
// Ограничиваем минимальную и максимальную высоту
|
||||
int minHeight = (int) dp(40);
|
||||
int maxHeight = ((ViewGroup) getParent()).getHeight() / 2;
|
||||
|
||||
newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
|
||||
|
||||
if (newHeight != lp.height) {
|
||||
lp.height = newHeight;
|
||||
dockHeightPx = newHeight;
|
||||
setLayoutParams(lp);
|
||||
|
||||
// Если закреплен снизу, нужно также изменить позицию Y
|
||||
if (!dockTop) {
|
||||
float newY = ((ViewGroup) getParent()).getHeight() - newHeight;
|
||||
setY(newY);
|
||||
}
|
||||
|
||||
if (dockResizeListener != null) {
|
||||
dockResizeListener.onDockResize(newHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDocking(MotionEvent event) {
|
||||
// Проверяем расстояние до краев экрана
|
||||
float screenHeight = ((ViewGroup) getParent()).getHeight();
|
||||
float y = event.getRawY();
|
||||
|
||||
float dockThreshold = dp(100);
|
||||
|
||||
if (y < dockThreshold) {
|
||||
// Близко к верхнему краю
|
||||
targetDragX = 0f;
|
||||
targetDragY = 0f;
|
||||
} else if (y > screenHeight - dockThreshold) {
|
||||
// Близко к нижнему краю
|
||||
targetDragX = 0f;
|
||||
targetDragY = screenHeight - getHeight();
|
||||
} else {
|
||||
targetDragX = null;
|
||||
targetDragY = null;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean shouldDock(MotionEvent event) {
|
||||
return targetDragX != null && targetDragY != null;
|
||||
}
|
||||
|
||||
private void performDocking(MotionEvent event) {
|
||||
float screenHeight = ((ViewGroup) getParent()).getHeight();
|
||||
float y = event.getRawY();
|
||||
|
||||
boolean dockToTop = y < screenHeight / 2;
|
||||
|
||||
// При докинге всегда устанавливаем размер по умолчанию
|
||||
dockHeightPx = 0; // Сбрасываем сохраненную высоту
|
||||
|
||||
setDocked(true, dockToTop, 0f, dockToTop ? 0f : screenHeight - dp(DEFAULT_DOCK_HEIGHT_DP));
|
||||
}
|
||||
|
||||
private float getDistance(MotionEvent event) {
|
||||
if (event.getPointerCount() < 2) return 0;
|
||||
|
||||
float x = event.getX(0) - event.getX(1);
|
||||
float y = event.getY(0) - event.getY(1);
|
||||
return (float) Math.sqrt(x * x + y * y);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||
if (isDocked) {
|
||||
int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
int height = dockHeightPx > 0 ? dockHeightPx : (int) dp(DEFAULT_DOCK_HEIGHT_DP);
|
||||
setMeasuredDimension(width, height);
|
||||
} else {
|
||||
int size = (int)(dp(CIRCLE_SIZE_DP) * scaleFactor);
|
||||
setMeasuredDimension(size, size);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
// Вызываем соответствующий метод отрисовки
|
||||
if (isDocked) {
|
||||
onDrawDock(canvas);
|
||||
} else {
|
||||
onDrawCircle(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public void setDocked(boolean docked, boolean top) {
|
||||
setDocked(docked, top, getX(), getY());
|
||||
}
|
||||
|
||||
public void setDocked(boolean docked, boolean top, float targetX, float targetY) {
|
||||
if (this.isDocked == docked && this.dockTop == top && getX() == targetX && getY() == targetY) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dockTop = top;
|
||||
|
||||
if (morphAnimator != null && morphAnimator.isRunning()) {
|
||||
morphAnimator.cancel();
|
||||
}
|
||||
|
||||
float startMorph = morphProgress;
|
||||
float endMorph = docked ? 0f : 1f;
|
||||
|
||||
int startW = getWidth();
|
||||
int startH = getHeight();
|
||||
|
||||
ViewGroup parent = (ViewGroup) getParent();
|
||||
int parentWidth = parent.getWidth();
|
||||
int parentHeight = parent.getHeight();
|
||||
int dockHeight = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
|
||||
int circleSize = (int) dp(CIRCLE_SIZE_DP);
|
||||
|
||||
int endW = docked ? parentWidth : circleSize;
|
||||
int endH = docked ? dockHeight : circleSize;
|
||||
|
||||
float startX = getX();
|
||||
float startY = getY();
|
||||
float endX = targetX;
|
||||
float endY = targetY;
|
||||
|
||||
// Если доким в нижнюю часть, корректируем позицию Y
|
||||
if (docked && !top) {
|
||||
endY = parentHeight - dockHeight;
|
||||
}
|
||||
|
||||
// Сохраняем финальные значения для использования в lambda и inner class
|
||||
final float finalStartX = startX;
|
||||
final float finalStartY = startY;
|
||||
final float finalEndX = endX;
|
||||
final float finalEndY = endY;
|
||||
|
||||
morphAnimator = ValueAnimator.ofFloat(0f, 1f);
|
||||
morphAnimator.setDuration(350);
|
||||
morphAnimator.addUpdateListener(anim -> {
|
||||
float t = (float) anim.getAnimatedValue();
|
||||
morphProgress = startMorph + (endMorph - startMorph) * t;
|
||||
|
||||
int w = (int) (startW + (endW - startW) * t);
|
||||
int h = (int) (startH + (endH - startH) * t);
|
||||
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
lp.width = w;
|
||||
lp.height = h;
|
||||
setLayoutParams(lp);
|
||||
|
||||
setX(finalStartX + (finalEndX - finalStartX) * t);
|
||||
setY(finalStartY + (finalEndY - finalStartY) * t);
|
||||
|
||||
postInvalidateOnAnimation();
|
||||
});
|
||||
|
||||
morphAnimator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
lp.width = endW;
|
||||
lp.height = endH;
|
||||
setLayoutParams(lp);
|
||||
|
||||
setX(finalEndX);
|
||||
setY(finalEndY);
|
||||
morphProgress = endMorph;
|
||||
|
||||
postInvalidateOnAnimation();
|
||||
|
||||
isMorphing = false;
|
||||
}
|
||||
});
|
||||
|
||||
morphAnimator.start();
|
||||
this.isDocked = docked;
|
||||
isMorphing = true;
|
||||
}
|
||||
|
||||
public boolean isDocked() {
|
||||
return isDocked;
|
||||
}
|
||||
|
||||
public boolean isDockTop() {
|
||||
return dockTop;
|
||||
}
|
||||
|
||||
protected boolean isMorphing() {
|
||||
return isMorphing;
|
||||
}
|
||||
|
||||
public void setOnDockResizeListener(OnDockResizeListener listener) {
|
||||
this.dockResizeListener = listener;
|
||||
}
|
||||
|
||||
protected float dp(float dp) {
|
||||
return dp * getResources().getDisplayMetrics().density;
|
||||
}
|
||||
|
||||
// Абстрактные методы для переопределения в наследниках
|
||||
protected abstract void onDrawDock(Canvas canvas);
|
||||
protected abstract void onDrawCircle(Canvas canvas);
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
package com.grigowashere.aismap.view;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.utils.GeoUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class CompassView extends BaseDockWidget {
|
||||
private static final String TAG = "CompassView";
|
||||
|
||||
private float targetAzimuth = 0;
|
||||
private float currentAzimuth = 0;
|
||||
private float magneticCompass = 0; // магнитный компас
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint vesselPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path vesselPath = new Path();
|
||||
private final String[] directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
|
||||
private float centerX;
|
||||
private float centerY;
|
||||
private static final float SMOOTHING_FACTOR = 0.15f;
|
||||
private List<AISVessel> nearbyVessels = new ArrayList<>();
|
||||
private Vessel ourVessel; // наше судно для расчета расстояний
|
||||
private static final float MAX_DISPLAY_DISTANCE = 10000; // 10 км
|
||||
private static final float MIN_VESSEL_SIZE = 10; // минимальный размер значка
|
||||
private static final float MAX_VESSEL_SIZE = 30; // максимальный размер значка
|
||||
|
||||
public CompassView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CompassView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTextSize(36f);
|
||||
|
||||
vesselPaint.setStyle(Paint.Style.FILL);
|
||||
vesselPaint.setAntiAlias(true);
|
||||
|
||||
// Устанавливаем фон для видимости
|
||||
setBackgroundColor(Color.argb(200, 0, 0, 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
centerX = w / 2f;
|
||||
centerY = h / 2f;
|
||||
}
|
||||
|
||||
private float getShortestRotation(float start, float end) {
|
||||
float diff = end - start;
|
||||
while (diff > 180) diff -= 360;
|
||||
while (diff < -180) diff += 360;
|
||||
return diff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Прямая шкала (dock-режим)
|
||||
@Override
|
||||
protected void onDrawDock(Canvas canvas) {
|
||||
Log.d(TAG, "onDrawDock called, width=" + getWidth() + ", height=" + getHeight());
|
||||
|
||||
float w = getWidth();
|
||||
float h = getHeight();
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
|
||||
return;
|
||||
}
|
||||
|
||||
// Простой фон для начала
|
||||
paint.setColor(Color.argb(200, 0, 0, 0));
|
||||
canvas.drawRect(0, 0, w, h, paint);
|
||||
|
||||
// Масштабируем размеры в зависимости от высоты виджета
|
||||
float baseHeight = dp(80); // базовая высота
|
||||
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
|
||||
|
||||
// Простой текст для проверки
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextSize(24 * scaleFactor);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText("КОМПАС", w/2, h/2, paint);
|
||||
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
|
||||
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
|
||||
|
||||
// Плавное обновление азимута
|
||||
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
||||
if (Math.abs(diff) > 0.1f) {
|
||||
currentAzimuth += diff * SMOOTHING_FACTOR;
|
||||
if (currentAzimuth > 360) currentAzimuth -= 360;
|
||||
if (currentAzimuth < 0) currentAzimuth += 360;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
// Рисуем простую шкалу
|
||||
float centerX = w / 2f;
|
||||
float centerY = h / 2f;
|
||||
float visibleDegrees = 120;
|
||||
|
||||
// Рисуем деления шкалы
|
||||
for (int degree = 0; degree < 360; degree += 15) {
|
||||
// Вычисляем относительное положение деления
|
||||
float relativeDegree = (degree - currentAzimuth + 360) % 360;
|
||||
if (relativeDegree > 180) relativeDegree -= 360;
|
||||
|
||||
// Рисуем только видимые деления
|
||||
if (Math.abs(relativeDegree) <= visibleDegrees / 2) {
|
||||
float x = centerX + (relativeDegree / (visibleDegrees / 2)) * (w / 2);
|
||||
float lineHeight = (degree % 30 == 0) ? 20 * scaleFactor : 10 * scaleFactor;
|
||||
canvas.drawLine(x, centerY - lineHeight, x, centerY + lineHeight, paint);
|
||||
|
||||
if (degree % 30 == 0) {
|
||||
String degreeText = String.valueOf(degree);
|
||||
paint.setTextSize(16 * scaleFactor);
|
||||
canvas.drawText(degreeText, x, centerY - 30 * scaleFactor, paint);
|
||||
}
|
||||
if (degree % 45 == 0) {
|
||||
int directionIndex = (degree / 45) % 8;
|
||||
if (directionIndex < directions.length) {
|
||||
paint.setTextSize(18 * scaleFactor);
|
||||
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Рисуем суда
|
||||
for (AISVessel vessel : nearbyVessels) {
|
||||
float relativeBearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
|
||||
if (relativeBearing > 180) relativeBearing -= 360;
|
||||
if (Math.abs(relativeBearing) <= visibleDegrees / 2) {
|
||||
float x = centerX + (relativeBearing / (visibleDegrees / 2)) * (w / 2);
|
||||
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
|
||||
float size = calculateVesselSize((float) distance) * scaleFactor;
|
||||
vesselPaint.setColor(getVesselColor(vessel));
|
||||
drawVesselTriangle(canvas, x, centerY, size, (float) (vessel.getCourse() - currentAzimuth));
|
||||
}
|
||||
}
|
||||
|
||||
// Центральная линия (направление вперёд)
|
||||
paint.setColor(Color.RED);
|
||||
paint.setStrokeWidth(3 * scaleFactor);
|
||||
canvas.drawLine(centerX, centerY - h/2, centerX, centerY + h/2, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setStrokeWidth(1);
|
||||
|
||||
// Выделяем зону resize в зависимости от позиции закрепления
|
||||
if (isDocked) {
|
||||
Paint resizePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
resizePaint.setColor(Color.argb(120, 255, 255, 255));
|
||||
resizePaint.setStyle(Paint.Style.STROKE);
|
||||
resizePaint.setStrokeWidth(2);
|
||||
|
||||
paint.setTextSize(12);
|
||||
paint.setColor(Color.WHITE);
|
||||
|
||||
if (isDockTop()) {
|
||||
// Если закреплен сверху, показываем зону resize снизу
|
||||
canvas.drawRect(0, h - dp(24), w, h, resizePaint);
|
||||
canvas.drawText("↕", w/2, h - dp(12), paint);
|
||||
} else {
|
||||
// Если закреплен снизу, показываем зону resize сверху
|
||||
canvas.drawRect(0, 0, w, dp(24), resizePaint);
|
||||
canvas.drawText("↕", w/2, dp(12), paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Круглый компас (draggable-режим)
|
||||
@Override
|
||||
protected void onDrawCircle(Canvas canvas) {
|
||||
Log.d(TAG, "onDrawCircle called, width=" + getWidth() + ", height=" + getHeight());
|
||||
|
||||
float w = getWidth();
|
||||
float h = getHeight();
|
||||
|
||||
if (w <= 0 || h <= 0) {
|
||||
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
|
||||
return;
|
||||
}
|
||||
|
||||
float cx = w / 2f;
|
||||
float cy = h / 2f;
|
||||
float radius = Math.min(w, h) / 2f * 0.9f;
|
||||
|
||||
// Масштабируем размеры в зависимости от размера виджета
|
||||
float baseSize = dp(120); // базовая высота
|
||||
float scaleFactor = Math.max(0.8f, Math.min(2.0f, Math.min(w, h) / baseSize));
|
||||
|
||||
// Фон
|
||||
paint.setColor(Color.argb(200, 0, 0, 0));
|
||||
canvas.drawCircle(cx, cy, radius, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
|
||||
// Плавное обновление азимута
|
||||
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
||||
if (Math.abs(diff) > 0.1f) {
|
||||
currentAzimuth += diff * SMOOTHING_FACTOR;
|
||||
if (currentAzimuth > 360) currentAzimuth -= 360;
|
||||
if (currentAzimuth < 0) currentAzimuth += 360;
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
// Деления и метки по кругу
|
||||
for (int degree = 0; degree < 360; degree += 30) {
|
||||
float angle = (float) Math.toRadians(degree - currentAzimuth);
|
||||
float x1 = cx + (float) Math.sin(angle) * (radius * 0.85f);
|
||||
float y1 = cy - (float) Math.cos(angle) * (radius * 0.85f);
|
||||
float x2 = cx + (float) Math.sin(angle) * radius;
|
||||
float y2 = cy - (float) Math.cos(angle) * radius;
|
||||
paint.setStrokeWidth(2 * scaleFactor);
|
||||
canvas.drawLine(x1, y1, x2, y2, paint);
|
||||
|
||||
if (degree % 90 == 0) {
|
||||
int directionIndex = (degree / 90) % 4;
|
||||
String[] mainDirections = {"N", "E", "S", "W"};
|
||||
float dx = cx + (float) Math.sin(angle) * (radius - 25 * scaleFactor);
|
||||
float dy = cy - (float) Math.cos(angle) * (radius - 25 * scaleFactor);
|
||||
paint.setTextSize(16 * scaleFactor);
|
||||
canvas.drawText(mainDirections[directionIndex], dx, dy, paint);
|
||||
}
|
||||
}
|
||||
|
||||
// Рисуем суда по кругу
|
||||
for (AISVessel vessel : nearbyVessels) {
|
||||
float bearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
|
||||
float angle = (float) Math.toRadians(bearing);
|
||||
float vesselRadius = radius * 0.6f;
|
||||
float vx = cx + (float) Math.sin(angle) * vesselRadius;
|
||||
float vy = cy - (float) Math.cos(angle) * vesselRadius;
|
||||
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
|
||||
float size = calculateVesselSize((float) distance) * scaleFactor;
|
||||
vesselPaint.setColor(getVesselColor(vessel));
|
||||
drawVesselTriangle(canvas, vx, vy, size, (float) (vessel.getCourse() - currentAzimuth));
|
||||
}
|
||||
|
||||
// Центральная линия (направление вперёд)
|
||||
paint.setColor(Color.RED);
|
||||
paint.setStrokeWidth(3 * scaleFactor);
|
||||
canvas.drawLine(cx, cy, cx, cy - radius, paint);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setStrokeWidth(1);
|
||||
|
||||
// Текст азимута в центре
|
||||
paint.setTextSize(14 * scaleFactor);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText((int)currentAzimuth + "°", cx, cy + 5 * scaleFactor, paint);
|
||||
|
||||
|
||||
}
|
||||
|
||||
private float calculateVesselSize(float distance) {
|
||||
if (distance > MAX_DISPLAY_DISTANCE) return MIN_VESSEL_SIZE;
|
||||
// Линейная интерполяция размера от расстояния
|
||||
float ratio = 1 - Math.min(distance / MAX_DISPLAY_DISTANCE, 1);
|
||||
return MIN_VESSEL_SIZE + (MAX_VESSEL_SIZE - MIN_VESSEL_SIZE) * ratio;
|
||||
}
|
||||
|
||||
private int getVesselColor(AISVessel vessel) {
|
||||
// Можно настроить цвета в зависимости от параметров судна
|
||||
// Используем navigation status из AIS данных
|
||||
int navStatus = GeoUtils.getNavigationStatusCode(vessel.getNavigationalStatus());
|
||||
switch (navStatus) {
|
||||
case 0: // Under way using engine
|
||||
return Color.GREEN;
|
||||
case 1: // At anchor
|
||||
return Color.YELLOW;
|
||||
case 5: // Moored
|
||||
return Color.BLUE;
|
||||
default:
|
||||
return Color.WHITE;
|
||||
}
|
||||
}
|
||||
|
||||
private void drawVesselTriangle(Canvas canvas, float x, float y, float size, float rotation) {
|
||||
vesselPath.reset();
|
||||
|
||||
// Создаем треугольник
|
||||
float halfSize = size / 2;
|
||||
vesselPath.moveTo(x, y - halfSize); // вершина
|
||||
vesselPath.lineTo(x - halfSize, y + halfSize); // левый нижний угол
|
||||
vesselPath.lineTo(x + halfSize, y + halfSize); // правый нижний угол
|
||||
vesselPath.close();
|
||||
|
||||
// Поворачиваем треугольник
|
||||
canvas.save();
|
||||
canvas.rotate(rotation, x, y);
|
||||
canvas.drawPath(vesselPath, vesselPaint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
public void setAzimuth(float azimuth) {
|
||||
this.targetAzimuth = azimuth;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setMagneticCompass(float magneticCompass) {
|
||||
this.magneticCompass = magneticCompass;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void updateNearbyVessels(List<AISVessel> vessels) {
|
||||
this.nearbyVessels = vessels;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setOurVessel(Vessel ourVessel) {
|
||||
this.ourVessel = ourVessel;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user