Initial commit: AIS Map Android application

This commit is contained in:
ОС Программист
2025-09-02 15:58:16 +03:00
commit 629b403dd2
78 changed files with 9209 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+53
View File
@@ -0,0 +1,53 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.grigowashere.aismap'
compileSdk 35
defaultConfig {
applicationId "com.grigowashere.aismap"
minSdk 30
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Яндекс.Карты
implementation 'com.yandex.android:maps.mobile:4.4.0-full'
implementation group: 'org.mapsforge', name: 'mapsforge-map-android', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-themes', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-map', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-map-reader', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.25.0'
// Тестирование
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package com.grigowashere.aismap;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.grigowashere.aismap", appContext.getPackageName());
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Разрешения для GPS -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Разрешения для интернета (для Яндекс.Карт) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Разрешения для UDP -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Разрешения для записи в файл (для логирования) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Дополнительные разрешения для GPS -->
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-feature android:name="android.hardware.location" android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AISMap"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/Theme.AISMap">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Мета-данные для Яндекс.Карт -->
<meta-data
android:name="com.yandex.mapkit.ApiKey"
android:value="9ae1917c-2049-4927-9d1e-29dd0d3e8ebc" />
</application>
</manifest>
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
import: https://mapzen.com/carto/bubble-wrap-style/9/bubble-wrap-style.zip
sources:
osm:
type: MVT
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png
max_zoom: 19
layers:
earth:
data: { source: osm }
draw:
background:
color: '#f8f4f0'
Binary file not shown.
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();
}
}
+13
View File
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,3l-13,26l13,-5l13,5z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
+101
View File
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- Карта -->
<com.yandex.mapkit.mapview.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Панель управления -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_margin="16dp"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="8dp"
android:elevation="4dp">
<Button
android:id="@+id/btn_center_vessel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Центр на судне"
android:textSize="12sp"
android:minWidth="100dp" />
<Button
android:id="@+id/btn_test_compass"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Тест компаса"
android:textSize="12sp"
android:minWidth="100dp"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- Компас -->
<com.grigowashere.aismap.view.CompassView
android:id="@+id/compass_view"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_alignParentTop="true"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="0dp" />
<!-- Простая информационная панель
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_margin="16dp"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="12dp"
android:elevation="4dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="139dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Статус: Инициализация..."
android:textColor="@android:color/black"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_ais_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🚢 AIS суда: 0"
android:textSize="12sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btn_show_vessel_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📋 Информация о судне"
android:textSize="11sp"
android:minHeight="36dp"
android:background="@android:color/holo_blue_light"
android:textColor="@android:color/white" />
</LinearLayout> -->
</RelativeLayout>
@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="16dp">
<!-- Заголовок с кнопкой закрытия -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/bottom_sheet_ais_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="🚢 AIS СУДНО"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black" />
<ImageButton
android:id="@+id/btn_close_ais_bottom_sheet"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Закрыть" />
</LinearLayout>
<!-- Основная информация -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="400dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- MMSI -->
<TextView
android:id="@+id/bottom_sheet_ais_mmsi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🆔 MMSI: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Название судна -->
<TextView
android:id="@+id/bottom_sheet_ais_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📛 Название: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Позывной -->
<TextView
android:id="@+id/bottom_sheet_ais_callsign"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📻 Позывной: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- IMO -->
<TextView
android:id="@+id/bottom_sheet_ais_imo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🏷️ IMO: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Тип судна -->
<TextView
android:id="@+id/bottom_sheet_ais_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🚢 Тип: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Координаты -->
<TextView
android:id="@+id/bottom_sheet_ais_position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📍 Координаты: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Курс -->
<TextView
android:id="@+id/bottom_sheet_ais_course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🧭 Курс: --°"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Скорость -->
<TextView
android:id="@+id/bottom_sheet_ais_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚡ Скорость: -- узлов"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Размеры -->
<TextView
android:id="@+id/bottom_sheet_ais_dimensions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📏 Размеры: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Осадка -->
<TextView
android:id="@+id/bottom_sheet_ais_draft"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🌊 Осадка: -- м"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Пункт назначения -->
<TextView
android:id="@+id/bottom_sheet_ais_destination"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🎯 Назначение: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- ETA -->
<TextView
android:id="@+id/bottom_sheet_ais_eta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⏰ ETA: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Навигационный статус -->
<TextView
android:id="@+id/bottom_sheet_ais_nav_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🚦 Статус: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Класс судна -->
<TextView
android:id="@+id/bottom_sheet_ais_class"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📋 Класс: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Сила сигнала -->
<TextView
android:id="@+id/bottom_sheet_ais_signal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📶 Сигнал: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Последнее обновление -->
<TextView
android:id="@+id/bottom_sheet_ais_last_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🕐 Обновлено: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Время назад -->
<TextView
android:id="@+id/bottom_sheet_ais_time_ago"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⏱️ Время назад: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="16dp">
<!-- Заголовок с кнопкой закрытия -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="🚢 НАШЕ СУДНО"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black" />
<ImageButton
android:id="@+id/btn_close_bottom_sheet"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Закрыть" />
</LinearLayout>
<!-- Основная информация -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="400dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Статус -->
<TextView
android:id="@+id/bottom_sheet_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Статус: Инициализация..."
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Координаты -->
<TextView
android:id="@+id/bottom_sheet_position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📍 Координаты: Не определены"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Курс -->
<TextView
android:id="@+id/bottom_sheet_course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🧭 Курс: --°"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Скорость -->
<TextView
android:id="@+id/bottom_sheet_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚡ Скорость: -- узлов"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Высота -->
<TextView
android:id="@+id/bottom_sheet_altitude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🏔️ Высота: -- м"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Точность -->
<TextView
android:id="@+id/bottom_sheet_accuracy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🎯 Точность: -- м"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Качество GPS -->
<TextView
android:id="@+id/bottom_sheet_gps_quality"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📊 Качество GPS: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Спутники -->
<TextView
android:id="@+id/bottom_sheet_satellites"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Спутники: --/--"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- DOP значения -->
<TextView
android:id="@+id/bottom_sheet_dop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📈 DOP: PDOP=-- HDOP=-- VDOP=--"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Время фикса -->
<TextView
android:id="@+id/bottom_sheet_fix_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🕐 Время фикса: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Качество фикса -->
<TextView
android:id="@+id/bottom_sheet_fix_quality"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔒 Качество фикса: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_gps"
android:title="GPS"
android:icon="@android:drawable/ic_menu_mylocation"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_udp"
android:title="UDP"
android:icon="@android:drawable/ic_menu_send"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_clear_ais"
android:title="Очистить AIS"
android:icon="@android:drawable/ic_menu_delete"
app:showAsAction="ifRoom" />
</menu>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" standalone="no"?>
<svg version="1.1" id="Icons" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32 32" xml:space="preserve" height="128" width="128">
<style type="text/css">
.st0{fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:10;}
</style>
<polygon class="st0" points="16,3 3,29 16,24 29,29 "/>
</svg>

After

Width:  |  Height:  |  Size: 419 B

+7
View File
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AISMap" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">AISMap</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AISMap" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.AISMap" parent="Base.Theme.AISMap" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.grigowashere.aismap;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}