commit 629b403dd2f8f1d8894de5619b94e15dce8b6e9d Author: ОС Программист Date: Tue Sep 2 15:58:16 2025 +0300 Initial commit: AIS Map Android application diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b86273d --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..639c779 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b2c751a --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..f79ea0f --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AIS_MESSAGE_TYPES.md b/AIS_MESSAGE_TYPES.md new file mode 100644 index 0000000..1314ae9 --- /dev/null +++ b/AIS_MESSAGE_TYPES.md @@ -0,0 +1,132 @@ +# Поддерживаемые типы AIS сообщений + +## Обзор +Данный документ описывает типы AIS (Automatic Identification System) сообщений, которые поддерживаются в приложении AISMap. + +## Основные типы сообщений + +### 1. Position Reports (Типы 1, 2, 3) +- **Описание**: Отчеты о позиции судов класса A +- **Содержит**: MMSI, координаты, курс, скорость, направление, статус +- **Использование**: Основная информация о движении судов + +### 2. Base Station Report (Тип 4) +- **Описание**: Отчет базовой станции +- **Содержит**: MMSI, дата/время, координаты, точность позиции, тип EPFD +- **Использование**: Информация о базовых станциях AIS + +### 3. Static Data (Тип 5) +- **Описание**: Статические данные о судне +- **Содержит**: MMSI, IMO, название, позывной, тип судна, размеры, пункт назначения +- **Использование**: Детальная информация о судне + +### 4. Safety Related Broadcast (Тип 14) +- **Описание**: Сообщения безопасности +- **Содержит**: MMSI, текст сообщения безопасности +- **Использование**: Важные сообщения о безопасности навигации + +### 5. Class B Position Report (Тип 18) +- **Описание**: Отчет о позиции судов класса B +- **Содержит**: MMSI, координаты, курс, скорость, направление, точность +- **Использование**: Информация о малых судах и яхтах + +### 6. Extended Class B Position Report (Тип 19) +- **Описание**: Расширенный отчет о позиции судов класса B +- **Содержит**: Все данные типа 18 + название, тип судна, размеры +- **Использование**: Подробная информация о малых судах + +### 7. Aid-to-Navigation Report (Тип 21) +- **Описание**: Отчет о навигационных знаках +- **Содержит**: MMSI, тип знака, название, координаты, размеры +- **Использование**: Информация о маяках, буях и других навигационных знаках + +### 8. Static Data Report (Тип 24) +- **Описание**: Отчет о статических данных (разделенный на части) +- **Часть A**: Название судна +- **Часть B**: Тип судна, производитель, позывной, размеры +- **Использование**: Дополнительная статическая информация + +## Структура данных + +### Поля MMSI +- **Размер**: 30 бит +- **Описание**: Уникальный идентификатор судна +- **Диапазон**: 0 - 999999999 + +### Координаты +- **Широта**: 27 бит, диапазон -90° до +90° +- **Долгота**: 28 бит, диапазон -180° до +180° +- **Точность**: 1/60000 градуса (~1.85 метра) + +### Скорость и курс +- **Скорость**: 10 бит, диапазон 0-102.3 узла +- **Курс**: 12 бит, диапазон 0-359.9° +- **Направление**: 9 бит, диапазон 0-359° + +### Размеры судна +- **Длина**: 10 бит, диапазон 0-1023 метра +- **Ширина**: 10 бит, диапазон 0-1023 метра +- **Осадка**: 8 бит, диапазон 0-25.5 метра + +## Обработка фрагментов + +### Многочастные сообщения +- Поддержка сообщений, разделенных на несколько частей +- Автоматическое объединение фрагментов +- Таймаут для устаревших фрагментов (10 секунд) + +### Контрольные суммы +- Проверка контрольных сумм NMEA сообщений +- Отбрасывание сообщений с неверными контрольными суммами + +## Логирование + +### Уровни логирования +- **DEBUG**: Детальная информация о декодировании +- **INFO**: Основные события (создание судов, обновления) +- **WARN**: Предупреждения (неверные данные, устаревшие фрагменты) +- **ERROR**: Ошибки декодирования + +### Информация в логах +- Тип сообщения и MMSI +- Декодированные значения +- Ошибки парсинга +- Статистика по спутникам + +## Производительность + +### Оптимизации +- Кэширование декодированных судов +- Автоматическая очистка устаревших данных +- Эффективные регулярные выражения для парсинга + +### Мониторинг +- Количество активных AIS судов +- Время последнего обновления +- Статус активности судов + +## Совместимость + +### Стандарты +- IEC 61993-2 (AIS Class A) +- IEC 62287 (AIS Class B) +- NMEA 0183 (протокол передачи) + +### Оборудование +- AIS трансиверы класса A и B +- Базовые станции +- Навигационные знаки с AIS +- Мобильные устройства с AIS + +## Расширение функциональности + +### Добавление новых типов +1. Добавить case в метод `decodeAISPayload` +2. Создать метод декодирования +3. Обновить модель `AISVessel` при необходимости +4. Добавить логирование и обработку ошибок + +### Кастомные поля +- Поддержка региональных расширений +- Обработка производитель-специфичных данных +- Гибкая система метаданных diff --git a/GPS_HYBRID_APPROACH.md b/GPS_HYBRID_APPROACH.md new file mode 100644 index 0000000..49381f1 --- /dev/null +++ b/GPS_HYBRID_APPROACH.md @@ -0,0 +1,142 @@ +# Гибридный GPS подход в AISMap + +## Проблема + +Изначально приложение пыталось получать все GPS данные через NMEA сообщения, но столкнулось с нестабильностью получения RMC сообщений (курс, скорость). NMEA сообщения могут приходить нерегулярно или вообще не приходить, что делает невозможным получение критически важной навигационной информации. + +## Решение: Гибридный подход + +Мы реализовали **гибридный подход**, который комбинирует два источника данных: + +### 1. Android Location API (основной источник координат) +- **Что получаем**: Координаты (широта/долгота), точность, время фикса, качество фикса +- **Преимущества**: Стабильный, надежный, работает на всех устройствах +- **Класс**: `GPSLocationListener` + +### 2. NMEA сообщения (дополнительные данные) +- **Что получаем**: Курс, скорость, количество спутников, DOP (PDOP/HDOP/VDOP), высота +- **Преимущества**: Детальная навигационная информация, стандарт морской навигации +- **Класс**: `AndroidNMEAListener` + `NMEAParser` + +## Архитектура + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ MainActivity │ │ AppController │ │ MapInterface │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ │ │ + │ │ │ + ▼ ▼ ▼ +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ GPSLocation │ │ NMEAParser │ │ Карта │ +│ Listener │ │ (гибридный) │ │ │ +│ (координаты) │ │ (курс, DOP) │ │ │ +└─────────────────┘ └──────────────────┘ └─────────────────┘ + │ │ + │ │ + ▼ ▼ +┌─────────────────┐ ┌──────────────────┐ +│ Android NMEA │ │ UDP Listener │ +│ Listener │ │ (AIS данные) │ +│ (NMEA сообщения)│ │ │ +└─────────────────┘ └──────────────────┘ +``` + +## Ключевые компоненты + +### GPSLocationListener +- Получает координаты через `LocationManager.requestLocationUpdates()` +- Отслеживает количество спутников через `GnssStatus.Callback` +- Предоставляет точность позиции в метрах +- Определяет качество фикса (GPS, DGPS, RTK) + +### NMEAParser (гибридный режим) +- В гибридном режиме **НЕ обновляет координаты** из NMEA +- Парсит только дополнительные данные: + - **GGA**: количество спутников, высота + - **RMC**: курс, скорость + - **VTG**: курс, скорость (альтернативный источник) + - **GSA**: DOP значения, активные спутники + - **GSV**: спутники в поле зрения + +### AppController +- Координирует работу всех компонентов +- Объединяет данные из разных источников в единый объект `Vessel` +- Управляет жизненным циклом слушателей + +## Преимущества гибридного подхода + +### ✅ Надежность +- Координаты всегда доступны через Location API +- Не зависит от стабильности NMEA сообщений + +### ✅ Полнота данных +- Получаем все необходимые навигационные параметры +- DOP значения для оценки качества GPS + +### ✅ Совместимость +- Работает на всех версиях Android +- Поддерживает как старый GPS API, так и новый GNSS API + +### ✅ Производительность +- Location API оптимизирован системой +- NMEA парсинг только для дополнительных данных + +## Использование + +### Включение гибридного режима +```java +// В AppController +nmeaParser.setHybridMode(true); +nmeaParser.setGPSLocationListener(gpsLocationListener); + +// Запуск слушателей +appController.setGPSLocationEnabled(true); // Для координат +appController.setAndroidNMEAEnabled(true); // Для дополнительных данных +``` + +### Получение данных +```java +Vessel vessel = appController.getOwnVessel(); + +// Координаты (из Location API) +double lat = vessel.getLatitude(); +double lon = vessel.getLongitude(); +float accuracy = vessel.getAccuracy(); + +// Навигационные данные (из NMEA) +double course = vessel.getCourse(); +double speed = vessel.getSpeed(); +int satellites = vessel.getSatellites(); + +// Качество GPS (из NMEA + Location API) +double pdop = vessel.getPdop(); +double hdop = vessel.getHdop(); +double vdop = vessel.getVdop(); +String quality = vessel.getGPSQualityDescription(); +``` + +## Тестирование + +Для тестирования создан класс `GPSHybridTest`, который демонстрирует работу гибридного подхода: + +```java +GPSHybridTest test = new GPSHybridTest(context); +test.startTest(); // Запускает тест +test.stopTest(); // Останавливает тест +test.cleanup(); // Освобождает ресурсы +``` + +## Логирование + +Приложение ведет детальное логирование для отладки: + +- `📍` - GPS Location данные +- `📡` - NMEA данные +- `📊` - DOP значения +- `🛰️` - Информация о спутниках +- `🚢` - Статус судна + +## Заключение + +Гибридный подход решает проблему нестабильности NMEA сообщений, обеспечивая надежное получение координат через Location API и дополнительных навигационных данных через NMEA. Это дает нам лучшее из двух миров: надежность Android системы и богатство морских навигационных данных. diff --git a/GPS_NMEA_TROUBLESHOOTING.md b/GPS_NMEA_TROUBLESHOOTING.md new file mode 100644 index 0000000..c6497e9 --- /dev/null +++ b/GPS_NMEA_TROUBLESHOOTING.md @@ -0,0 +1,148 @@ +# Решение проблем с NMEA сообщениями в Android + +## Проблема +RMC пакеты то приходят, то не приходят, особенно при старте приложения. Это типичная проблема холодного запуска GPS в Android. + +## Причины проблемы + +### 1. Холодный запуск GPS +- **GPS модуль включен** ≠ **GPS активен и отправляет данные** +- Android может держать GPS в "спящем" режиме для экономии батареи +- NMEA сообщения не отправляются без активных слушателей + +### 2. Отсутствие активного запроса локации +- `addNmeaListener()` регистрирует слушатель, но не активирует GPS +- Нужен активный `requestLocationUpdates()` для "пробуждения" GPS +- Без этого NMEA может не приходить часами + +### 3. Проблемы с регистрацией слушателей +- Неправильный порядок регистрации +- Отсутствие обработки ошибок +- Проблемы с версиями Android API + +## Решения + +### 1. Улучшенная регистрация NMEA слушателя +```java +// Регистрируем с Handler для лучшей производительности +locationManager.addNmeaListener(this, mainHandler); + +// Сразу запрашиваем обновления локации для активации GPS +requestLocationUpdates(); +``` + +### 2. Агрессивная активация GPS +```java +// Минимальные интервалы для быстрой активации +locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, + 100, // 100мс вместо 1000мс + 0, // 0 метров + locationListener, + mainHandler +); + +// Дополнительно запрашиваем одиночное обновление +locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER, ...); +``` + +### 3. Обработка GPS событий +```java +// При старте GNSS сразу активируем GPS +@Override +public void onStarted() { + requestLocationUpdates(); // Активируем GPS +} + +// При первом фиксировании также активируем +@Override +public void onFirstFix(int ttffMillis) { + requestLocationUpdates(); // Дополнительная активация +} +``` + +### 4. Поддержка старых версий Android +```java +if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + // Старый API - GpsStatus.Listener + locationManager.addGpsStatusListener(gpsStatusListener); +} else { + // Новый API - GnssStatus.Callback + locationManager.registerGnssStatusCallback(gnssStatusCallback, mainHandler); +} +``` + +## Диагностика + +### Логи для отслеживания +``` +=== ДИАГНОСТИКА GPS === +Доступные провайдеры: + gps: включен + network: включен + passive: включен + +=== РЕГИСТРАЦИЯ СЛУШАТЕЛЕЙ === +✅ NMEA слушатель зарегистрирован +✅ GNSS статус callback зарегистрирован (новый API) +✅ Location updates запрошены с минимальным интервалом (100мс) +✅ Одиночное обновление запрошено + +🎯 NMEA [RMC] получено: $GPRMC,123456,A,... +🚢 RMC сообщение получено - GPS активен! +``` + +### Проверка состояния +```java +// Детальная информация о GPS +String gpsInfo = nmeaListener.getGPSStatusInfo(); +Log.i(TAG, gpsInfo); + +// Принудительная активация +nmeaListener.forceGPSActivation(); +``` + +## Рекомендации + +### 1. При запуске приложения +- Сразу регистрируйте NMEA слушатель +- Запрашивайте location updates с минимальными интервалами +- Обрабатывайте GPS события для дополнительной активации + +### 2. При проблемах с получением NMEA +- Проверьте логи на наличие ошибок +- Убедитесь, что GPS провайдер включен +- Попробуйте принудительную активацию GPS +- Проверьте количество видимых спутников + +### 3. Оптимизация +- Используйте минимальные интервалы только при необходимости +- Обрабатывайте ошибки и повторяйте попытки +- Мониторьте состояние GPS через callback'и + +## Тестирование + +Используйте `NMEATestHelper` для диагностики: +```java +NMEATestHelper testHelper = new NMEATestHelper(context); +testHelper.startTest(); // Запускает тест с периодической диагностикой +testHelper.forceGPSActivation(); // Принудительная активация +testHelper.stopTest(); // Остановка теста +``` + +## Частые проблемы + +1. **"GPS провайдер отключен"** - включите GPS в настройках +2. **"Нет разрешения ACCESS_FINE_LOCATION"** - запросите разрешение +3. **"LocationManager недоступен"** - проверьте инициализацию +4. **NMEA не приходит** - проверьте логи, попробуйте принудительную активацию + +## Заключение + +Проблема с RMC пакетами решается правильной последовательностью: +1. Регистрация NMEA слушателя +2. Активация GPS через location updates +3. Обработка GPS событий +4. Диагностика и мониторинг + +После этих изменений NMEA сообщения должны приходить стабильно, даже при холодном запуске GPS. diff --git a/README.md b/README.md new file mode 100644 index 0000000..34e9a2a --- /dev/null +++ b/README.md @@ -0,0 +1,157 @@ +# AISMap - Приложение для отображения AIS и GPS данных на карте + +## Описание + +AISMap - это Android приложение для отображения данных о судах, полученных через AIS (Automatic Identification System) и GPS, на интерактивной карте. Приложение поддерживает несколько источников данных и различные SDK карт. + +## Архитектура + +### Модели данных + +#### Vessel.java +Модель для нашего судна, содержащая: +- Координаты (широта, долгота) +- Курс и скорость +- Сила GPS сигнала +- Количество спутников +- Высота над уровнем моря +- Временная метка последнего обновления + +#### AISVessel.java +Модель для AIS судов с расширенной информацией: +- MMSI (Maritime Mobile Service Identity) +- Название судна и позывной +- IMO номер и тип судна +- Размеры (длина, ширина, осадка) +- Пункт назначения и ETA +- Навигационный статус + +### Контроллеры + +#### NMEAParser.java +Парсер NMEA сообщений, поддерживающий: +- GGA (Global Positioning System Fix Data) +- RMC (Recommended Minimum Navigation Information) +- VTG (Course Over Ground and Ground Speed) +- AIS (Automatic Identification System) +- Валидация контрольных сумм +- Callback интерфейс для уведомлений + +#### UDPListener.java +Контроллер для прослушивания UDP портов: +- Асинхронное прослушивание +- Настраиваемый порт +- Отправка и получение данных +- Обработка ошибок + +#### AndroidNMEAListener.java +Контроллер для Android NMEA Listener: +- Использование встроенного GPS +- Поддержка старых версий Android +- Мониторинг статуса GPS +- Получение количества спутников + +#### AppController.java +Главный контроллер, координирующий: +- Все источники данных +- Обновление моделей +- Взаимодействие с картой +- Управление жизненным циклом + +### Интерфейс карт + +#### MapInterface.java +Абстрактный интерфейс для карт: +- Добавление/обновление меток судов +- Управление слоями +- Обработка кликов +- Центрирование и зум + +#### YandexMapImpl.java +Реализация для Яндекс.Карт: +- Использование API ключа +- Создание иконок судов +- Анимации перемещения +- Обработка событий + +#### MapForgeImpl.java +Реализация для MapForge (офлайн карты): +- Работа с локальными картами +- Слои и маркеры +- Оптимизация производительности + +## Источники данных + +### UDP +- Порт по умолчанию: 10110 (стандартный для AIS) +- Настраиваемый порт +- Асинхронная обработка + +### Android NMEA Listener +- Встроенный GPS модуль +- Прямой доступ к NMEA данным +- Единообразие с UDP данными + +### BLE (планируется) +- Поддержка внешних GPS устройств +- Bluetooth Low Energy +- Автоматическое обнаружение + +## Особенности + +### Валидация данных +- Проверка контрольных сумм NMEA +- Фильтрация некорректных сообщений +- Логирование ошибок + +### Производительность +- Асинхронная обработка данных +- Кэширование меток +- Оптимизация обновлений карты + +### Расширяемость +- Модульная архитектура +- Поддержка новых источников данных +- Легкое добавление новых SDK карт + +## Настройка + +### Зависимости +```gradle +// Яндекс.Карты +implementation 'com.yandex.android:maps.mobile:4.4.0-full' + +// MapForge (опционально) +implementation 'org.mapsforge:mapsforge-map-android:0.15.0' +``` + +### Разрешения +```xml + + + +``` + +### API ключи +- Яндекс.Карты: `9ae1917c-2049-4927-9d1e-29dd0d3e8ebc` + +## Использование + +1. Запустите приложение +2. Предоставьте разрешения на GPS +3. Включите GPS слушатель +4. При необходимости включите UDP слушатель +5. Используйте кнопки управления для навигации + +## Планы развития + +- [ ] Поддержка BLE устройств +- [ ] Расширенный декодер AIS +- [ ] Сохранение истории маршрутов +- [ ] Экспорт данных +- [ ] Настройки отображения +- [ ] Многоязычная поддержка + +## Лицензия + +Проект разработан для образовательных целей. diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..855f6e0 --- /dev/null +++ b/app/build.gradle @@ -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' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/app/src/androidTest/java/com/grigowashere/aismap/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/grigowashere/aismap/ExampleInstrumentedTest.java new file mode 100644 index 0000000..3475dc5 --- /dev/null +++ b/app/src/androidTest/java/com/grigowashere/aismap/ExampleInstrumentedTest.java @@ -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 Testing documentation + */ +@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()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..145b29e --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/assets/crimean-fed-district.map b/app/src/main/java/assets/crimean-fed-district.map new file mode 100644 index 0000000..0dd8f02 Binary files /dev/null and b/app/src/main/java/assets/crimean-fed-district.map differ diff --git a/app/src/main/java/assets/scene.yaml b/app/src/main/java/assets/scene.yaml new file mode 100644 index 0000000..c8d96d4 --- /dev/null +++ b/app/src/main/java/assets/scene.yaml @@ -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' \ No newline at end of file diff --git a/app/src/main/java/assets/world.map b/app/src/main/java/assets/world.map new file mode 100644 index 0000000..03f9fd2 Binary files /dev/null and b/app/src/main/java/assets/world.map differ diff --git a/app/src/main/java/com/grigowashere/aismap/MainActivity.java b/app/src/main/java/com/grigowashere/aismap/MainActivity.java new file mode 100644 index 0000000..3448c35 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/MainActivity.java @@ -0,0 +1,1085 @@ +package com.grigowashere.aismap; + +import android.Manifest; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import com.google.android.material.bottomsheet.BottomSheetDialog; + +import com.grigowashere.aismap.controllers.AppController; +import com.grigowashere.aismap.controllers.MapController; +import com.grigowashere.aismap.maps.MapInterface; +import com.grigowashere.aismap.models.Vessel; +import com.grigowashere.aismap.models.AISVessel; +import com.grigowashere.aismap.sensors.CompassSensor; +import com.grigowashere.aismap.view.CompassView; +import com.yandex.mapkit.mapview.MapView; +import java.util.List; +import java.util.ArrayList; + +public class MainActivity extends AppCompatActivity { + + private static final String TAG = "MainActivity"; + private static final int PERMISSION_REQUEST_CODE = 1001; + + // Статическая переменная для отслеживания инициализации Яндекс.Карт + private static boolean isYandexMapsInitialized = false; + + private AppController appController; + private MapController mapController; + private MapInterface mapInterface; + private MapView mapView; + + private Button btnCenterOnVessel; + private Button btnTestCompass; + private CompassView compassView; + private CompassSensor compassSensor; + + // BottomSheet для отображения информации о нашем судне + private BottomSheetDialog ownVesselBottomSheet; + private View bottomSheetView; + + // BottomSheet для отображения информации об AIS судне + private BottomSheetDialog aisVesselBottomSheet; + private View aisBottomSheetView; + private AISVessel currentAISVessel; // Текущее AIS судно в BottomSheet + private android.os.Handler timeUpdateHandler; // Handler для обновления времени + private Runnable timeUpdateRunnable; // Runnable для обновления времени + + // Автоматическое обновление BottomSheet + private android.os.Handler bottomSheetUpdateHandler; // Handler для обновления BottomSheet + private Runnable bottomSheetUpdateRunnable; // Runnable для обновления BottomSheet + private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000; // Обновление каждую секунду + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Инициализируем Яндекс.Карты перед setContentView + initializeYandexMaps(); + + setContentView(R.layout.activity_main); + + initializeViews(); + initializeControllers(); + // checkPermissions() будет вызван в onStart + } + + private void initializeViews() { + mapView = findViewById(R.id.map_view); + btnCenterOnVessel = findViewById(R.id.btn_center_vessel); + btnTestCompass = findViewById(R.id.btn_test_compass); + compassView = findViewById(R.id.compass_view); + + // Инициализируем магнитный компас + compassSensor = new CompassSensor(this); + + initializeBottomSheet(); + setupButtonListeners(); + setupCompass(); + } + + private void setupButtonListeners() { + btnCenterOnVessel.setOnClickListener(v -> centerOnVessel()); + btnTestCompass.setOnClickListener(v -> testCompass()); + + // Кнопка для показа информации о судне +// Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info); +// if (btnShowVesselInfo != null) { +// btnShowVesselInfo.setOnClickListener(v -> showOwnVesselBottomSheet()); +// } + } + + private void setupCompass() { + // Устанавливаем начальный азимут (например, север) + compassView.setAzimuth(0); + + // Устанавливаем компас в dock-режим вверху экрана + compassView.post(() -> { + compassView.setDocked(true, true, 0, 0); + compassView.invalidate(); // Принудительная отрисовка + }); + + // Настраиваем слушатель изменения размера док-виджета + compassView.setOnDockResizeListener(newHeight -> { + Log.d(TAG, "Compass dock height changed to: " + newHeight); + }); + + // Настраиваем магнитный компас + if (compassSensor.isAvailable()) { + compassSensor.startListening(new CompassSensor.CompassListener() { + + @Override + public void onCompassChanged(float azimuth) { + // Обновляем компас в UI потоке + runOnUiThread(() -> { + compassView.setAzimuth(azimuth); + compassView.setMagneticCompass(azimuth); + + // Обновляем магнитный компас в модели нашего судна + if (appController != null) { + Vessel ourVessel = appController.getOwnVessel(); + if (ourVessel != null) { + ourVessel.setMagneticCompass(azimuth); + } + } + }); + } + }); + Log.d(TAG, "Magnetic compass started"); + } else { + Log.w(TAG, "Magnetic compass not available"); + } + + // Принудительная отрисовка + compassView.invalidate(); + } + + private void onUpdateCompass(float azimuth, List nearbyVessels) { + if (compassView != null) { + compassView.setAzimuth(azimuth); + compassView.updateNearbyVessels(nearbyVessels); + } + } + + /** + * Инициализирует BottomSheet для отображения информации о нашем судне + */ + private void initializeBottomSheet() { + // Инициализация Handler для обновления времени + timeUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + timeUpdateRunnable = new Runnable() { + @Override + public void run() { + if (currentAISVessel != null && aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing()) { + updateAISTimeAgo(); + } + // Планируем следующее обновление через 1 секунду + timeUpdateHandler.postDelayed(this, 1000); + } + }; + + // Инициализация Handler для автоматического обновления BottomSheet + bottomSheetUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + bottomSheetUpdateRunnable = new Runnable() { + @Override + public void run() { + // Обновляем BottomSheet нашего судна, если он открыт + if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { + updateBottomSheetUI(); + } + + // Обновляем AIS BottomSheet, если он открыт + if (aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing() && currentAISVessel != null) { + updateAISBottomSheetUI(currentAISVessel); + } + + // Планируем следующее обновление + bottomSheetUpdateHandler.postDelayed(this, BOTTOM_SHEET_UPDATE_INTERVAL); + } + }; + + // Инициализация BottomSheet для нашего судна + ownVesselBottomSheet = new BottomSheetDialog(this); + bottomSheetView = getLayoutInflater().inflate(R.layout.bottom_sheet_own_vessel, null); + ownVesselBottomSheet.setContentView(bottomSheetView); + + // Настраиваем кнопку закрытия + ImageButton btnClose = bottomSheetView.findViewById(R.id.btn_close_bottom_sheet); + btnClose.setOnClickListener(v -> { + ownVesselBottomSheet.dismiss(); + // Восстанавливаем обработчики кликов после закрытия + restoreMarkerClickListeners(); + // Останавливаем автоматическое обновление + stopBottomSheetAutoUpdate(); + }); + + // Настраиваем поведение BottomSheet + ownVesselBottomSheet.setCanceledOnTouchOutside(true); + ownVesselBottomSheet.setCancelable(true); + + // Добавляем слушатель закрытия BottomSheet + ownVesselBottomSheet.setOnDismissListener(dialog -> { + // Восстанавливаем обработчики кликов после закрытия + restoreMarkerClickListeners(); + // Останавливаем автоматическое обновление + stopBottomSheetAutoUpdate(); + }); + + // Инициализация BottomSheet для AIS судов + aisVesselBottomSheet = new BottomSheetDialog(this); + aisBottomSheetView = getLayoutInflater().inflate(R.layout.bottom_sheet_ais_vessel, null); + aisVesselBottomSheet.setContentView(aisBottomSheetView); + + // Настраиваем кнопку закрытия для AIS BottomSheet + ImageButton btnCloseAIS = aisBottomSheetView.findViewById(R.id.btn_close_ais_bottom_sheet); + btnCloseAIS.setOnClickListener(v -> { + aisVesselBottomSheet.dismiss(); + stopTimeUpdate(); + // Восстанавливаем обработчики кликов после закрытия + restoreMarkerClickListeners(); + // Останавливаем автоматическое обновление + stopBottomSheetAutoUpdate(); + }); + + // Настраиваем поведение AIS BottomSheet + aisVesselBottomSheet.setCanceledOnTouchOutside(true); + aisVesselBottomSheet.setCancelable(true); + + // Добавляем слушатель закрытия BottomSheet + aisVesselBottomSheet.setOnDismissListener(dialog -> { + stopTimeUpdate(); + // Восстанавливаем обработчики кликов после закрытия + restoreMarkerClickListeners(); + // Останавливаем автоматическое обновление + stopBottomSheetAutoUpdate(); + }); + } + + private void initializeControllers() { + // Инициализация главного контроллера + appController = new AppController(this); + + // Инициализация контроллера карты + mapController = new MapController(this); + + // Устанавливаем callback для обновления UI + appController.setUIUpdateCallback(new AppController.ExtendedUIUpdateCallback() { + @Override + public void onVesselPositionUpdated(Vessel vessel) { + updateVesselPositionUI(vessel); + // Обновляем наше судно в компасе + if (compassView != null) { + compassView.setOurVessel(vessel); + } + } + + @Override + public void onGPSQualityUpdated(Vessel vessel) { + updateGPSQualityUI(vessel); + } + + @Override + public void onShowOwnVesselBottomSheet() { + Log.i(TAG, "onShowOwnVesselBottomSheet callback получен в MainActivity"); + showOwnVesselBottomSheet(); + } + + @Override + public void onShowAISVesselInfo(AISVessel vessel) { + showAISVesselBottomSheet(vessel); + } + + @Override + public void onUpdateCompass(float azimuth, List nearbyVessels) { + if (compassView != null) { + compassView.setAzimuth(azimuth); + compassView.updateNearbyVessels(nearbyVessels); + } + } + }); + } + + private void startControllers() { + // Включаем GPS и UDP по умолчанию + appController.setGPSLocationEnabled(true); + appController.setAndroidNMEAEnabled(true); + appController.setUDPEnabled(true); + + // Запускаем все слушатели + appController.startAllListeners(); + } + + /** + * Обновляет статус в UI + */ + private void updateStatusUI() { + // Обновляем статус в UI +// TextView tvStatus = findViewById(R.id.tv_status); +// TextView tvAisCount = findViewById(R.id.tv_ais_count); +// +// if (tvStatus != null) { +// tvStatus.setText("Статус: GPS активен, UDP готов"); +// } +// +// if (tvAisCount != null) { +// tvAisCount.setText("AIS суда: 0"); +// } + } + + /** + * Обновляет позицию судна в UI + */ + private void updateVesselPositionUI(Vessel vessel) { + runOnUiThread(() -> { + if (vessel == null) return; + + // Обновляем статус +// TextView tvStatus = findViewById(R.id.tv_status); +// if (tvStatus != null) { +// tvStatus.setText("Статус: GPS активен, данные получены"); +// } + + // Обновляем BottomSheet, если он открыт + if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { + updateBottomSheetUI(); + } + }); + } + + /** + * Обновляет качество GPS в UI + */ + private void updateGPSQualityUI(Vessel vessel) { + runOnUiThread(() -> { + if (vessel == null) return; + + // Обновляем BottomSheet, если он открыт + if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { + updateBottomSheetUI(); + } + }); + } + + private void toggleUDP() { + boolean isEnabled = appController.isUDPEnabled(); + + if (isEnabled) { + appController.setUDPEnabled(false); + Toast.makeText(this, "UDP слушатель отключен", Toast.LENGTH_SHORT).show(); + } else { + appController.setUDPEnabled(true); + Toast.makeText(this, "UDP слушатель включен", Toast.LENGTH_SHORT).show(); + } + + // Обновляем заголовок меню + invalidateOptionsMenu(); + } + + private void toggleGPS() { + boolean isEnabled = appController.isAndroidNMEAEnabled(); + + if (isEnabled) { + appController.setAndroidNMEAEnabled(false); + Toast.makeText(this, "GPS слушатель отключен", Toast.LENGTH_SHORT).show(); + } else { + appController.setAndroidNMEAEnabled(true); + Toast.makeText(this, "GPS слушатель включен", Toast.LENGTH_SHORT).show(); + } + + // Обновляем заголовок меню + invalidateOptionsMenu(); + } + + private void centerOnVessel() { + appController.centerOnOwnVessel(); + Toast.makeText(this, "Карта центрирована на судне", Toast.LENGTH_SHORT).show(); + } + + private void testCompass() { + if (compassView != null) { + // Создаем тестовые AIS суда + List testVessels = new ArrayList<>(); + + AISVessel testVessel1 = new AISVessel("123456789"); + testVessel1.setLatitude(59.9343); + testVessel1.setLongitude(30.3351); + testVessel1.setCourse(45); + testVessel1.setSpeed(10); + testVessel1.setNavigationalStatus("under way using engine"); + testVessels.add(testVessel1); + + AISVessel testVessel2 = new AISVessel("987654321"); + testVessel2.setLatitude(59.9343); + testVessel2.setLongitude(30.3351); + testVessel2.setCourse(180); + testVessel2.setSpeed(5); + testVessel2.setNavigationalStatus("at anchor"); + testVessels.add(testVessel2); + + compassView.updateNearbyVessels(testVessels); + + // Проверяем доступность магнитного компаса + if (compassSensor.isAvailable()) { + Toast.makeText(this, "Магнитный компас доступен и работает", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Магнитный компас недоступен", Toast.LENGTH_SHORT).show(); + } + } + } + + private void clearAIS() { + appController.clearAISVessels(); + Toast.makeText(this, "AIS суда очищены", Toast.LENGTH_SHORT).show(); + } + + @Override + protected void onStart() { + super.onStart(); + + // Запускаем карту через контроллер + if (mapController != null) { + Log.i(TAG, "Запускаем карту..."); + mapController.startMap(); + + // Инициализируем карту + Log.i(TAG, "Инициализируем карту..."); + mapInterface = mapController.initializeMap("yandex", mapView); + Log.i(TAG, "mapInterface получен: " + (mapInterface != null ? "успешно" : "null")); + + // Устанавливаем интерфейс карты в главный контроллер + if (mapInterface != null) { + Log.i(TAG, "Устанавливаем mapInterface в AppController..."); + appController.setMapInterface(mapInterface); + Log.i(TAG, "mapInterface установлен в AppController"); + + mapInterface.initialize(); + Log.i(TAG, "Карта инициализирована"); + + // Проверяем, что все настроено правильно + Log.i(TAG, "Проверяем настройку карты..."); + + // Дополнительная проверка обработчиков кликов + Log.i(TAG, "Проверяем обработчики кликов..."); + if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) { + com.grigowashere.aismap.maps.YandexMapImpl yandexMap = (com.grigowashere.aismap.maps.YandexMapImpl) mapInterface; + yandexMap.refreshMarkerClickListeners(); + Log.i(TAG, "Обработчики кликов обновлены"); + } + } else { + Log.e(TAG, "Не удалось получить mapInterface!"); + } + } + + // Проверяем разрешения и запускаем контроллеры + checkPermissions(); + } + + @Override + protected void onStop() { + super.onStop(); + + // Останавливаем карту + if (mapInterface != null) { + mapInterface.cleanup(); + } + + // Останавливаем все слушатели + if (appController != null) { + appController.stopAllListeners(); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + // Останавливаем обновление времени + stopTimeUpdate(); + + // Останавливаем автоматическое обновление BottomSheet + stopBottomSheetAutoUpdate(); + + // Останавливаем магнитный компас + if (compassSensor != null) { + compassSensor.stopListening(); + } + + // Освобождаем ресурсы + if (appController != null) { + appController.cleanup(); + } + + if (mapController != null) { + mapController.cleanup(); + } + } + + @Override + public void onConfigurationChanged(android.content.res.Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + // Обрабатываем изменения конфигурации (например, поворот экрана) + if (mapInterface != null) { + // Можно добавить логику для обработки изменений конфигурации карты + } + } + + /** + * Проверяет необходимые разрешения + */ + private void checkPermissions() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + + ActivityCompat.requestPermissions(this, + new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, + PERMISSION_REQUEST_CODE); + } else { + // Разрешения уже получены, запускаем контроллеры + startControllers(); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode == PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + // Разрешение получено, запускаем контроллеры + startControllers(); + } else { + // Разрешение не получено + Toast.makeText(this, "Для работы приложения необходимо разрешение на доступ к местоположению", + Toast.LENGTH_LONG).show(); + } + } + } + + // Меню + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.main_menu, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + // Обновляем состояние элементов меню + MenuItem gpsItem = menu.findItem(R.id.menu_gps); + MenuItem udpItem = menu.findItem(R.id.menu_udp); + + if (gpsItem != null) { + gpsItem.setTitle(appController.isAndroidNMEAEnabled() ? "GPS ✓" : "GPS"); + } + + if (udpItem != null) { + udpItem.setTitle(appController.isUDPEnabled() ? "UDP ✓" : "UDP"); + } + + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.menu_gps) { + toggleGPS(); + return true; + } else if (id == R.id.menu_udp) { + toggleUDP(); + return true; + } else if (id == R.id.menu_clear_ais) { + clearAIS(); + return true; + } + + return super.onOptionsItemSelected(item); + } + + /** + * Показывает BottomSheet с информацией о нашем судне + */ + private void showOwnVesselBottomSheet() { + if (ownVesselBottomSheet != null && !ownVesselBottomSheet.isShowing()) { + updateBottomSheetUI(); + ownVesselBottomSheet.show(); + + // Запускаем автоматическое обновление BottomSheet + startBottomSheetAutoUpdate(); + } + } + + /** + * Обновляет UI BottomSheet с актуальными данными + */ + private void updateBottomSheetUI() { + if (bottomSheetView == null) return; + + Vessel vessel = appController.getOwnVessel(); + if (vessel == null) return; + + // Убеждаемся, что обновление происходит в главном потоке + runOnUiThread(() -> { + if (bottomSheetView == null) return; + + Vessel currentVessel = appController.getOwnVessel(); + if (currentVessel == null) return; + + // Обновляем все поля в BottomSheet + TextView tvStatus = bottomSheetView.findViewById(R.id.bottom_sheet_status); + TextView tvPosition = bottomSheetView.findViewById(R.id.bottom_sheet_position); + TextView tvCourse = bottomSheetView.findViewById(R.id.bottom_sheet_course); + TextView tvSpeed = bottomSheetView.findViewById(R.id.bottom_sheet_speed); + TextView tvAltitude = bottomSheetView.findViewById(R.id.bottom_sheet_altitude); + TextView tvAccuracy = bottomSheetView.findViewById(R.id.bottom_sheet_accuracy); + TextView tvGPSQuality = bottomSheetView.findViewById(R.id.bottom_sheet_gps_quality); + TextView tvSatellites = bottomSheetView.findViewById(R.id.bottom_sheet_satellites); + TextView tvDOP = bottomSheetView.findViewById(R.id.bottom_sheet_dop); + TextView tvFixTime = bottomSheetView.findViewById(R.id.bottom_sheet_fix_time); + TextView tvFixQuality = bottomSheetView.findViewById(R.id.bottom_sheet_fix_quality); + + // Статус + if (tvStatus != null) { + if (currentVessel.getLatitude() != 0 && currentVessel.getLongitude() != 0) { + tvStatus.setText("Статус: GPS активен, данные получены"); + } else { + tvStatus.setText("Статус: Ожидание GPS данных..."); + } + } + + // Координаты + if (tvPosition != null) { + if (currentVessel.getLatitude() != 0 && currentVessel.getLongitude() != 0) { + String positionText = String.format("📍 Координаты: %.6f, %.6f", + currentVessel.getLatitude(), currentVessel.getLongitude()); + tvPosition.setText(positionText); + } else { + tvPosition.setText("📍 Координаты: Не определены"); + } + } + + // Курс + if (tvCourse != null) { + if (currentVessel.getCourse() > 0) { + String courseText = String.format("🧭 Курс: %.1f°", currentVessel.getCourse()); + tvCourse.setText(courseText); + } else { + tvCourse.setText("🧭 Курс: --°"); + } + } + + // Скорость + if (tvSpeed != null) { + if (currentVessel.getSpeed() > 0) { + String speedText = String.format("⚡ Скорость: %.1f узлов", currentVessel.getSpeed()); + tvSpeed.setText(speedText); + } else { + tvSpeed.setText("⚡ Скорость: -- узлов"); + } + } + + // Высота + if (tvAltitude != null) { + if (currentVessel.getAltitude() != 0) { + String altitudeText = String.format("🏔️ Высота: %.1f м", currentVessel.getAltitude()); + tvAltitude.setText(altitudeText); + } else { + tvAltitude.setText("🏔️ Высота: -- м"); + } + } + + // Точность + if (tvAccuracy != null) { + if (currentVessel.getAccuracy() > 0) { + String accuracyText = String.format("🎯 Точность: %.1f м", currentVessel.getAccuracy()); + tvAccuracy.setText(accuracyText); + } else { + tvAccuracy.setText("🎯 Точность: -- м"); + } + } + + // Качество GPS + if (tvGPSQuality != null) { + if (currentVessel.getGPSQualityDescription() != null) { + String qualityText = String.format("📊 Качество GPS: %s", currentVessel.getGPSQualityDescription()); + tvGPSQuality.setText(qualityText); + } else { + tvGPSQuality.setText("📊 Качество GPS: --"); + } + } + + // Спутники + if (tvSatellites != null) { + if (currentVessel.getSatellites() > 0) { + String satellitesText = String.format("Спутники: %d/%d", + currentVessel.getActiveSatellites(), currentVessel.getSatellites()); + tvSatellites.setText(satellitesText); + } else { + tvSatellites.setText("Спутники: --/--"); + } + } + + // DOP + if (tvDOP != null) { + if (currentVessel.getPdop() > 0) { + String dopText = String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", + currentVessel.getPdop(), currentVessel.getHdop(), currentVessel.getVdop()); + tvDOP.setText(dopText); + } else { + tvDOP.setText("📈 DOP: PDOP=-- HDOP=-- VDOP=--"); + } + } + + // Время фикса + if (tvFixTime != null) { + if (currentVessel.getFixTime() > 0) { + java.util.Date fixDate = new java.util.Date(currentVessel.getFixTime()); + String fixTimeText = String.format("🕐 Время фикса: %s", + new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(fixDate)); + tvFixTime.setText(fixTimeText); + } else { + tvFixTime.setText("🕐 Время фикса: --"); + } + } + + // Качество фикса + if (tvFixQuality != null) { + if (currentVessel.getFixQuality() != null) { + String fixQualityText = String.format("🔒 Качество фикса: %s", currentVessel.getFixQuality()); + tvFixQuality.setText(fixQualityText); + } else { + tvFixQuality.setText("🔒 Качество фикса: --"); + } + } + }); + } + + /** + * Показывает BottomSheet с информацией об AIS судне + */ + private void showAISVesselBottomSheet(AISVessel vessel) { + if (aisVesselBottomSheet != null && !aisVesselBottomSheet.isShowing()) { + currentAISVessel = vessel; + updateAISBottomSheetUI(vessel); + aisVesselBottomSheet.show(); + startTimeUpdate(); + + // Запускаем автоматическое обновление BottomSheet + startBottomSheetAutoUpdate(); + } + } + + /** + * Запускает обновление времени + */ + private void startTimeUpdate() { + if (timeUpdateHandler != null && timeUpdateRunnable != null) { + timeUpdateHandler.postDelayed(timeUpdateRunnable, 1000); + } + } + + /** + * Останавливает обновление времени + */ + private void stopTimeUpdate() { + if (timeUpdateHandler != null && timeUpdateRunnable != null) { + timeUpdateHandler.removeCallbacks(timeUpdateRunnable); + } + currentAISVessel = null; + } + + /** + * Запускает автоматическое обновление BottomSheet + */ + private void startBottomSheetAutoUpdate() { + if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) { + // Останавливаем предыдущее обновление, если оно запущено + bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable); + // Запускаем новое обновление + bottomSheetUpdateHandler.postDelayed(bottomSheetUpdateRunnable, BOTTOM_SHEET_UPDATE_INTERVAL); + Log.i(TAG, "Автоматическое обновление BottomSheet запущено"); + } + } + + /** + * Останавливает автоматическое обновление BottomSheet + */ + private void stopBottomSheetAutoUpdate() { + if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) { + bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable); + Log.i(TAG, "Автоматическое обновление BottomSheet остановлено"); + } + } + + /** + * Обновляет только время назад для AIS судна + */ + private void updateAISTimeAgo() { + if (aisBottomSheetView == null || currentAISVessel == null) return; + + TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago); + if (tvTimeAgo != null && currentAISVessel.getLastUpdate() != null) { + long secondsAgo = java.time.Duration.between(currentAISVessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds(); + String timeAgoText = formatTimeAgo(secondsAgo); + tvTimeAgo.setText("⏱️ Время назад: " + timeAgoText); + } + } + + /** + * Обновляет UI AIS BottomSheet с актуальными данными + */ + private void updateAISBottomSheetUI(AISVessel vessel) { + if (aisBottomSheetView == null || vessel == null) return; + + // Обновляем текущее судно, если это то же самое судно + if (currentAISVessel != null && currentAISVessel.getMmsi() != null && + currentAISVessel.getMmsi().equals(vessel.getMmsi())) { + currentAISVessel = vessel; + } + + // Убеждаемся, что обновление происходит в главном потоке + runOnUiThread(() -> { + if (aisBottomSheetView == null || vessel == null) return; + + // Обновляем все поля в AIS BottomSheet + TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title); + TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi); + TextView tvName = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_name); + TextView tvCallsign = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_callsign); + TextView tvImo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_imo); + TextView tvType = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_type); + TextView tvPosition = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_position); + TextView tvCourse = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_course); + TextView tvSpeed = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_speed); + TextView tvDimensions = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_dimensions); + TextView tvDraft = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_draft); + TextView tvDestination = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_destination); + TextView tvEta = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_eta); + TextView tvNavStatus = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_nav_status); + TextView tvClass = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_class); + TextView tvSignal = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_signal); + TextView tvLastUpdate = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_last_update); + TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago); + + // Заголовок + if (tvTitle != null) { + String title = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty() + ? "🚢 " + vessel.getVesselName() + : "🚢 AIS СУДНО"; + tvTitle.setText(title); + } + + // MMSI + if (tvMmsi != null) { + tvMmsi.setText("🆔 MMSI: " + (vessel.getMmsi() != null ? vessel.getMmsi() : "--")); + } + + // Название судна + if (tvName != null) { + tvName.setText("📛 Название: " + (vessel.getVesselName() != null ? vessel.getVesselName() : "--")); + } + + // Позывной + if (tvCallsign != null) { + tvCallsign.setText("📻 Позывной: " + (vessel.getCallSign() != null ? vessel.getCallSign() : "--")); + } + + // IMO + if (tvImo != null) { + tvImo.setText("🏷️ IMO: " + (vessel.getImo() > 0 ? String.valueOf(vessel.getImo()) : "--")); + } + + // Тип судна + if (tvType != null) { + tvType.setText("🚢 Тип: " + (vessel.getVesselType() != null ? vessel.getVesselType() : "--")); + } + + // Координаты + if (tvPosition != null) { + if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { + String positionText = String.format("📍 Координаты: %.6f, %.6f", + vessel.getLatitude(), vessel.getLongitude()); + tvPosition.setText(positionText); + } else { + tvPosition.setText("📍 Координаты: --"); + } + } + + // Курс + if (tvCourse != null) { + if (vessel.getCourse() > 0) { + String courseText = String.format("🧭 Курс: %.1f°", vessel.getCourse()); + tvCourse.setText(courseText); + } else { + tvCourse.setText("🧭 Курс: --°"); + } + } + + // Скорость + if (tvSpeed != null) { + if (vessel.getSpeed() > 0) { + String speedText = String.format("⚡ Скорость: %.1f узлов", vessel.getSpeed()); + tvSpeed.setText(speedText); + } else { + tvSpeed.setText("⚡ Скорость: -- узлов"); + } + } + + // Размеры + if (tvDimensions != null) { + if (vessel.getLength() > 0 && vessel.getWidth() > 0) { + String dimensionsText = String.format("📏 Размеры: %.1f x %.1f м", + vessel.getLength(), vessel.getWidth()); + tvDimensions.setText(dimensionsText); + } else { + tvDimensions.setText("📏 Размеры: --"); + } + } + + // Осадка + if (tvDraft != null) { + if (vessel.getDraft() > 0) { + String draftText = String.format("🌊 Осадка: %.1f м", vessel.getDraft()); + tvDraft.setText(draftText); + } else { + tvDraft.setText("🌊 Осадка: -- м"); + } + } + + // Пункт назначения + if (tvDestination != null) { + tvDestination.setText("🎯 Назначение: " + (vessel.getDestination() != null ? vessel.getDestination() : "--")); + } + + // ETA + if (tvEta != null) { + if (vessel.getEta() != null) { + String etaText = String.format("⏰ ETA: %s", + vessel.getEta().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))); + tvEta.setText(etaText); + } else { + tvEta.setText("⏰ ETA: --"); + } + } + + // Навигационный статус + if (tvNavStatus != null) { + tvNavStatus.setText("🚦 Статус: " + (vessel.getNavigationalStatus() != null ? vessel.getNavigationalStatus() : "--")); + } + + // Класс судна + if (tvClass != null) { + tvClass.setText("📋 Класс: " + (vessel.getVesselClass() != null ? vessel.getVesselClass() : "--")); + } + + // Сила сигнала + if (tvSignal != null) { + if (vessel.getSignalStrength() > 0) { + String signalText = String.format("📶 Сигнал: %d", vessel.getSignalStrength()); + tvSignal.setText(signalText); + } else { + tvSignal.setText("📶 Сигнал: --"); + } + } + + // Последнее обновление + if (tvLastUpdate != null) { + if (vessel.getLastUpdate() != null) { + String lastUpdateText = String.format("🕐 Обновлено: %s", + vessel.getLastUpdate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"))); + tvLastUpdate.setText(lastUpdateText); + } else { + tvLastUpdate.setText("🕐 Обновлено: --"); + } + } + + // Время назад + if (tvTimeAgo != null) { + if (vessel.getLastUpdate() != null) { + long secondsAgo = java.time.Duration.between(vessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds(); + String timeAgoText = formatTimeAgo(secondsAgo); + tvTimeAgo.setText("⏱️ Время назад: " + timeAgoText); + } else { + tvTimeAgo.setText("⏱️ Время назад: --"); + } + } + }); + } + + /** + * Форматирует время назад в читаемый вид + */ + private String formatTimeAgo(long seconds) { + if (seconds < 60) { + return seconds + " сек"; + } else if (seconds < 3600) { + long minutes = seconds / 60; + return minutes + " мин"; + } else if (seconds < 86400) { + long hours = seconds / 3600; + return hours + " ч"; + } else { + long days = seconds / 86400; + return days + " дн"; + } + } + + /** + * Восстанавливает обработчики кликов для маркеров + */ + private void restoreMarkerClickListeners() { + Log.i(TAG, "Восстанавливаем обработчики кликов для маркеров"); + if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) { + com.grigowashere.aismap.maps.YandexMapImpl yandexMap = (com.grigowashere.aismap.maps.YandexMapImpl) mapInterface; + yandexMap.refreshMarkerClickListeners(); + Log.i(TAG, "Обработчики кликов восстановлены"); + } + } + + /** + * Тестирует работу кликов по маркерам + */ + private void testMarkerClicks() { + Log.i(TAG, "Тестируем работу кликов по маркерам"); + Toast.makeText(this, "Тестируем клики по маркерам", Toast.LENGTH_SHORT).show(); + + // Восстанавливаем обработчики кликов + restoreMarkerClickListeners(); + + // Проверяем, что маркеры существуют + Vessel ownVessel = appController.getOwnVessel(); + if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) { + Log.i(TAG, "Наше судно найдено, координаты: " + ownVessel.getLatitude() + ", " + ownVessel.getLongitude()); + Toast.makeText(this, "Наше судно найдено, попробуйте кликнуть по маркеру", Toast.LENGTH_LONG).show(); + } else { + Log.w(TAG, "Наше судно не найдено или координаты равны 0"); + Toast.makeText(this, "Наше судно не найдено", Toast.LENGTH_SHORT).show(); + } + + // Проверяем AIS суда + List aisVessels = appController.getAISVessels(); + if (!aisVessels.isEmpty()) { + Log.i(TAG, "Найдено AIS судов: " + aisVessels.size()); + Toast.makeText(this, "Найдено AIS судов: " + aisVessels.size(), Toast.LENGTH_SHORT).show(); + } else { + Log.w(TAG, "AIS суда не найдены"); + Toast.makeText(this, "AIS суда не найдены", Toast.LENGTH_SHORT).show(); + } + } + + /** + * Инициализирует Яндекс.Карты + */ + private void initializeYandexMaps() { + if (!isYandexMapsInitialized) { + try { + // Инициализация Яндекс.Карт + com.yandex.mapkit.MapKitFactory.setApiKey("your_api_key_here"); + com.yandex.mapkit.MapKitFactory.initialize(this); + isYandexMapsInitialized = true; + + // Устанавливаем флаг в MapController + MapController.setYandexMapsInitialized(true); + + Log.i(TAG, "Яндекс.Карты успешно инициализированы"); + } catch (Exception e) { + Log.e(TAG, "Ошибка инициализации Яндекс.Карт: " + e.getMessage(), e); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/AndroidNMEAListener.java b/app/src/main/java/com/grigowashere/aismap/controllers/AndroidNMEAListener.java new file mode 100644 index 0000000..a41f311 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/AndroidNMEAListener.java @@ -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 + ")"; + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java b/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java new file mode 100644 index 0000000..aaa5b74 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java @@ -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 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 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 nearbyVessels = getNearbyVessels(); + + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + ((ExtendedUIUpdateCallback) uiUpdateCallback).onUpdateCompass(azimuth, nearbyVessels); + }); + } + } + + /** + * Получает список ближайших судов (в пределах 10 км) + */ + private List getNearbyVessels() { + List 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 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(); + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java b/app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java new file mode 100644 index 0000000..1d1d3c6 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java @@ -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(); + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/GPSLocationListener.java b/app/src/main/java/com/grigowashere/aismap/controllers/GPSLocationListener.java new file mode 100644 index 0000000..da758de --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/GPSLocationListener.java @@ -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; + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/MapController.java b/app/src/main/java/com/grigowashere/aismap/controllers/MapController.java new file mode 100644 index 0000000..5b2525a --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/MapController.java @@ -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(); + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java new file mode 100644 index 0000000..6afa31d --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java @@ -0,0 +1,1798 @@ +package com.grigowashere.aismap.controllers; + +import android.util.Log; +import com.grigowashere.aismap.models.Vessel; +import com.grigowashere.aismap.models.AISVessel; + +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.util.List; +import java.util.ArrayList; + +/** + * Контроллер для парсинга NMEA сообщений + * Работает в гибридном режиме: координаты через Location API, остальное через NMEA + */ +public class NMEAParser { + + private static final String TAG = "NMEAParser"; + + // Паттерны для NMEA сообщений + private static final Pattern GGA_PATTERN = Pattern.compile( + "\\$G[PN]GGA,(\\d{6}\\.\\d{2}),(\\d{4}\\.\\d+),([NS]),(\\d{5}\\.\\d+),([EW]),(\\d),(\\d+),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),\\*([0-9A-F]{2})" + ); + + private static final Pattern RMC_PATTERN = Pattern.compile( + "\\$G[PN]RMC,(\\d{6}\\.\\d{2}),([AV]),(\\d{4}\\.\\d+),([NS]),(\\d{5}\\.\\d+),([EW]),([^,]*),([^,]*),(\\d{6}),([^,]*),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" + ); + + private static final Pattern VTG_PATTERN = Pattern.compile( + "\\$G[PN]VTG,([^,]*),T,([^,]*),M,([^,]*),N,([^,]*),K\\*([0-9A-F]{2})" + ); + + private static final Pattern GLL_PATTERN = Pattern.compile( + "\\$G[PN]GLL,(\\d{4}\\.\\d{5}),([NS]),(\\d{5}\\.\\d{5}),([EW]),(\\d{6}),([AV]),([AV])\\*([0-9A-F]{2})" + ); + + private static final Pattern GSV_PATTERN = Pattern.compile( + "\\$G[APNLQ]GSV,(\\d+),(\\d+),(\\d+),(.*)\\*([0-9A-F]{2})" + ); + + private static final Pattern GNS_PATTERN = Pattern.compile( + "\\$GNGNS,(\\d{6}),(\\d{4}\\.\\d{5}),([NS]),(\\d{5}\\.\\d{5}),([EW]),(\\w+),(\\d+),(\\d+\\.\\d+),(\\d+\\.\\d+),([^,]*),([^,]*),([^,]*),([AV])\\*([0-9A-F]{2})" + ); + + // Паттерн для GSA сообщения (DOP и активные спутники) + private static final Pattern GSA_PATTERN = Pattern.compile( + "\\$G[PN]GSA,([AM]),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" + ); + + private static final Pattern AIS_PATTERN = Pattern.compile( + "!AIVDM,(\\d+),(\\d+),([^,]*),([AB12]),([^,]+),(\\d)\\*([0-9A-F]{2})" + ); + + private Vessel ownVessel; + private List aisVessels; + private NMEAParserListener listener; + private GPSLocationListener gpsLocationListener; + + // Поля для работы с AIS фрагментами + private java.util.Map> aisFragments = new java.util.HashMap<>(); + private java.util.Map aisFragmentTimestamps = new java.util.HashMap<>(); + private static final long AIS_FRAGMENT_TIMEOUT = 10000; // 10 секунд + + // Флаг для гибридного режима + private boolean hybridMode = true; + + // Поля для отслеживания спутников по системам + private int gpsSatellites = 0; + private int glonassSatellites = 0; + private int galileoSatellites = 0; + + public interface NMEAParserListener { + void onVesselUpdated(Vessel vessel); + void onAISVesselUpdated(AISVessel vessel); + void onParseError(String error); + void onDOPUpdated(double pdop, double hdop, double vdop); + } + + public NMEAParser() { + this.ownVessel = new Vessel(); + this.aisVessels = new ArrayList<>(); + } + + public void setListener(NMEAParserListener listener) { + this.listener = listener; + } + + /** + * Устанавливает GPS Location Listener для гибридного режима + */ + public void setGPSLocationListener(GPSLocationListener gpsLocationListener) { + this.gpsLocationListener = gpsLocationListener; + } + + /** + * Включает/выключает гибридный режим + */ + public void setHybridMode(boolean enabled) { + this.hybridMode = enabled; + Log.i(TAG, "Гибридный режим: " + (enabled ? "включен" : "отключен")); + } + + /** + * Парсит NMEA сообщение + */ + public void parseNMEA(String nmeaSentence) { + if (nmeaSentence == null || nmeaSentence.trim().isEmpty()) { + return; + } + + // Очищаем сообщение от лишних символов + String cleanedSentence = cleanNMEASentence(nmeaSentence); + Log.d(TAG, "Парсим NMEA: " + cleanedSentence); + + try { + if (cleanedSentence.startsWith("$GPGGA") || cleanedSentence.startsWith("$GNGGA")) { + parseGGA(cleanedSentence); + } else if (cleanedSentence.startsWith("$GPRMC") || cleanedSentence.startsWith("$GNRMC")) { + parseRMC(cleanedSentence); + } else if (cleanedSentence.startsWith("$GPVTG") || cleanedSentence.startsWith("$GNVTG")) { + parseVTG(cleanedSentence); + } else if (cleanedSentence.startsWith("$GPGLL") || cleanedSentence.startsWith("$GNGLL")) { + parseGLL(cleanedSentence); + } else if (cleanedSentence.startsWith("$GPGSV") || cleanedSentence.startsWith("$GAGSV") || cleanedSentence.startsWith("$GLGSV") || cleanedSentence.startsWith("$GNGSA")) { + parseGSV(cleanedSentence); + } else if (cleanedSentence.startsWith("$GNGNS")) { + parseGNS(cleanedSentence); + } else if (cleanedSentence.startsWith("$GPGSA") || cleanedSentence.startsWith("$GNGSA")) { + parseGSA(cleanedSentence); + } else if (cleanedSentence.startsWith("!AIVDM")) { + parseAIS(cleanedSentence); + } else { + Log.d(TAG, "Неподдерживаемый тип NMEA сообщения: " + cleanedSentence); + } + } catch (Exception e) { + Log.e(TAG, "Ошибка парсинга NMEA: " + e.getMessage(), e); + if (listener != null) { + listener.onParseError("Ошибка парсинга NMEA: " + e.getMessage()); + } + } + } + + /** + * Очищает NMEA сообщение от лишних символов + */ + private String cleanNMEASentence(String sentence) { + if (sentence == null) { + return null; + } + + // Убираем пробелы в начале и конце + String cleaned = sentence.trim(); + + // Убираем все символы после последнего * + int asteriskIndex = cleaned.lastIndexOf('*'); + if (asteriskIndex >= 0) { + cleaned = cleaned.substring(0, asteriskIndex + 3); // включаем * и 2 символа контрольной суммы + } + + // Убираем все непечатаемые символы + cleaned = cleaned.replaceAll("[^\\x20-\\x7E]", ""); + + Log.d(TAG, "Очищено NMEA: '" + cleaned + "' (длина: " + cleaned.length() + ")"); + + return cleaned; + } + + /** + * Парсит GGA сообщение (Global Positioning System Fix Data) + * В гибридном режиме используем только количество спутников и высоту + */ + private void parseGGA(String gga) { +// Log.d(TAG, "Парсим GGA: " + gga); +// Log.d(TAG, "Применяем паттерн GGA: " + GGA_PATTERN.pattern()); + Matcher matcher = GGA_PATTERN.matcher(gga); + if (matcher.matches()) { +// Log.d(TAG, "GGA совпадает с паттерном"); + + int satellites = Integer.parseInt(matcher.group(7)); + + // Обрабатываем высоту - может быть пустым полем (теперь в группе 8) + double altitude = 0.0; + String altitudeStr = matcher.group(8); + if (altitudeStr != null && !altitudeStr.trim().isEmpty()) { + try { + altitude = Double.parseDouble(altitudeStr); + } catch (NumberFormatException e) { + Log.w(TAG, "Не удалось распарсить высоту: '" + altitudeStr + "', используем 0.0"); + altitude = 0.0; + } + } + +// Log.d(TAG, String.format("GGA: sat=%d, alt=%.1f", satellites, altitude)); + + // В гибридном режиме не обновляем координаты + if (!hybridMode) { + // Обрабатываем координаты - могут быть пустыми полями (группы 2,3,4,5) + double latitude = 0.0; + double longitude = 0.0; + + String latStr = matcher.group(2); + String latDir = matcher.group(3); + if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { + latitude = parseCoordinate(latStr, latDir.equals("N")); + } + + String lonStr = matcher.group(4); + String lonDir = matcher.group(5); + if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { + longitude = parseCoordinate(lonStr, lonDir.equals("E")); + } + + ownVessel.setLatitude(latitude); + ownVessel.setLongitude(longitude); + } + + ownVessel.setSatellites(satellites); + ownVessel.setAltitude(altitude); + + // Синхронизируем с GPSLocationListener для получения активных спутников + if (gpsLocationListener != null) { + gpsLocationListener.setSatellitesInVessel(ownVessel); + } + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + } else { +// Log.w(TAG, "GGA не совпадает с паттерном"); + } + } + + /** + * Парсит RMC сообщение (Recommended Minimum Navigation Information) + * В гибридном режиме используем только курс и скорость + */ + private void parseRMC(String rmc) { +// Log.d(TAG, "Парсим RMC: " + rmc); +// Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern()); + + Matcher matcher = RMC_PATTERN.matcher(rmc); + if (matcher.matches()) { +// Log.d(TAG, "RMC совпадает с паттерном"); + + // Обрабатываем скорость - может быть пустым полем (теперь в группе 7) + double speed = 0.0; + String speedStr = matcher.group(7); + if (speedStr != null && !speedStr.trim().isEmpty()) { + try { + speed = Double.parseDouble(speedStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0"); + speed = 0.0; + } + } + + // Обрабатываем курс - может быть пустым полем (теперь в группе 8) + double course = 0.0; + String courseStr = matcher.group(8); + if (courseStr != null && !courseStr.trim().isEmpty()) { + try { + course = Double.parseDouble(courseStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0"); + course = 0.0; + } + } + +// Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f", speed, course)); + + // В гибридном режиме не обновляем координаты + if (!hybridMode) { + // Обрабатываем координаты - могут быть пустыми полями (группы 3,4,5,6) + double latitude = 0.0; + double longitude = 0.0; + + String latStr = matcher.group(3); + String latDir = matcher.group(4); + if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { + latitude = parseCoordinate(latStr, latDir.equals("N")); + } + + String lonStr = matcher.group(5); + String lonDir = matcher.group(6); + if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { + longitude = parseCoordinate(lonStr, lonDir.equals("E")); + } + + ownVessel.setLatitude(latitude); + ownVessel.setLongitude(longitude); + } + + ownVessel.setSpeed(speed); + ownVessel.setCourse(course); + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + } else { +// Log.w(TAG, "RMC не совпадает с паттерном"); + } + } + + /** + * Парсит VTG сообщение (Course Over Ground and Ground Speed) + */ + private void parseVTG(String vtg) { + Matcher matcher = VTG_PATTERN.matcher(vtg); + if (matcher.matches()) { + // Обрабатываем курс - может быть пустым полем + double course = 0.0; + String courseStr = matcher.group(2); + if (courseStr != null && !courseStr.trim().isEmpty()) { + try { + course = Double.parseDouble(courseStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить курс VTG: '" + courseStr + "', используем 0.0"); + course = 0.0; + } + } + + // Обрабатываем скорость - может быть пустым полем + double speed = 0.0; + String speedStr = matcher.group(4); + if (speedStr != null && !speedStr.trim().isEmpty()) { + try { + speed = Double.parseDouble(speedStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить скорость VTG: '" + speedStr + "', используем 0.0"); + speed = 0.0; + } + } + +// Log.d(TAG, String.format("VTG: course=%.1f, speed=%.1f", course, speed)); + + ownVessel.setCourse(course); + ownVessel.setSpeed(speed); + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + } + } + + /** + * Парсит GLL сообщение (Geographic Position - Latitude/Longitude) + * В гибридном режиме игнорируем + */ + private void parseGLL(String gll) { + if (hybridMode) { + Log.d(TAG, "GLL игнорируется в гибридном режиме"); + return; + } + +// Log.d(TAG, "Парсим GLL: " + gll); + Matcher matcher = GLL_PATTERN.matcher(gll); + if (matcher.matches()) { +// Log.d(TAG, "GLL совпадает с паттерном"); + // Обрабатываем координаты - могут быть пустыми полями + double latitude = 0.0; + double longitude = 0.0; + + String latStr = matcher.group(1); + String latDir = matcher.group(2); + if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { + latitude = parseCoordinate(latStr, latDir.equals("N")); + } + + String lonStr = matcher.group(3); + String lonDir = matcher.group(4); + if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { + longitude = parseCoordinate(lonStr, lonDir.equals("E")); + } + +// Log.d(TAG, String.format("GLL: lat=%.6f, lon=%.6f", latitude, longitude)); + + ownVessel.setLatitude(latitude); + ownVessel.setLongitude(longitude); + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + } else { +// Log.w(TAG, "GLL не совпадает с паттерном"); + } + } + + /** + * Парсит GSV сообщение (GPS Satellites in View) + */ + private void parseGSV(String gsv) { +// Log.d(TAG, "Парсим GSV: " + gsv); +// Log.d(TAG, "Применяем паттерн GSV: " + GSV_PATTERN.pattern()); + Matcher matcher = GSV_PATTERN.matcher(gsv); + if (matcher.matches()) { +// Log.d(TAG, "GSV совпадает с паттерном"); + int totalMessages = Integer.parseInt(matcher.group(1)); + int messageNumber = Integer.parseInt(matcher.group(2)); + int satellitesInView = Integer.parseInt(matcher.group(3)); + + // Определяем тип системы спутников + String systemType = "Unknown"; + if (gsv.startsWith("$GPGSV")) { + systemType = "GPS"; + } else if (gsv.startsWith("$GLGSV")) { + systemType = "GLONASS"; + } else if (gsv.startsWith("$GAGSV")) { + systemType = "Galileo"; + } else if (gsv.startsWith("$GNGSA")) { + systemType = "GNSS"; + } + +// Log.d(TAG, String.format("GSV [%s]: %d/%d, спутников в поле зрения: %d", +// systemType, messageNumber, totalMessages, satellitesInView)); + + // Парсим данные о спутниках из группы 4 + String satelliteData = matcher.group(4); + if (satelliteData != null && !satelliteData.trim().isEmpty()) { + String[] satFields = satelliteData.split(","); +// Log.d(TAG, String.format("Найдено %d полей данных о спутниках", satFields.length)); + + // Логируем информацию о спутниках (каждые 4 поля = 1 спутник) + for (int i = 0; i < satFields.length; i += 4) { + if (i + 3 < satFields.length) { + String satId = satFields[i]; + String elevation = satFields[i + 1]; + String azimuth = satFields[i + 2]; + String snr = satFields[i + 3]; + + if (!satId.trim().isEmpty()) { +// Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s", +// satId, elevation, azimuth, snr)); + } + } + } + } + + // GSV содержит информацию о спутниках, но не обновляет позицию + if (messageNumber == totalMessages) { + // Обновляем количество спутников для соответствующей системы + switch (systemType) { + case "GPS": + gpsSatellites = satellitesInView; + break; + case "GLONASS": + glonassSatellites = satellitesInView; + break; + case "Galileo": + galileoSatellites = satellitesInView; + break; + } + + // Обновляем общее количество спутников + int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites; + ownVessel.setSatellites(totalSatellites); + + // Синхронизируем с GPSLocationListener для получения активных спутников + if (gpsLocationListener != null) { + gpsLocationListener.setSatellitesInVessel(ownVessel); + } + +// Log.d(TAG, String.format("GSV [%s] завершен: %d спутников. Общий счет: GPS=%d, GLONASS=%d, Galileo=%d, Всего=%d", +// systemType, satellitesInView, gpsSatellites, glonassSatellites, galileoSatellites, totalSatellites)); + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + } + } else { +// Log.w(TAG, "GSV не совпадает с паттерном"); +// Log.d(TAG, "Сообщение: '" + gsv + "'"); +// Log.d(TAG, "Паттерн: " + GSV_PATTERN.pattern()); + } + } + + /** + * Парсит GNS сообщение (GNSS Fix Data) + * В гибридном режиме используем только количество спутников и высоту + */ + private void parseGNS(String gns) { + Log.d(TAG, "Парсим GNS: " + gns); + Matcher matcher = GNS_PATTERN.matcher(gns); + if (matcher.matches()) { +// Log.d(TAG, "GNS совпадает с паттерном"); + + int satellites = Integer.parseInt(matcher.group(7)); + + // Обрабатываем высоту - может быть пустым полем + double altitude = 0.0; + String altitudeStr = matcher.group(8); + if (altitudeStr != null && !altitudeStr.trim().isEmpty()) { + try { + altitude = Double.parseDouble(altitudeStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить высоту GNS: '" + altitudeStr + "', используем 0.0"); + altitude = 0.0; + } + } + +// Log.d(TAG, String.format("GNS: sat=%d, alt=%.1f", satellites, altitude)); + + // В гибридном режиме не обновляем координаты + if (!hybridMode) { + // Обрабатываем координаты - могут быть пустыми полями + double latitude = 0.0; + double longitude = 0.0; + + String latStr = matcher.group(2); + String latDir = matcher.group(3); + if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { + latitude = parseCoordinate(latStr, latDir.equals("N")); + } + + String lonStr = matcher.group(4); + String lonDir = matcher.group(5); + if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { + longitude = parseCoordinate(lonStr, lonDir.equals("E")); + } + + ownVessel.setLatitude(latitude); + ownVessel.setLongitude(longitude); + } + + ownVessel.setSatellites(satellites); + ownVessel.setAltitude(altitude); + + // Синхронизируем с GPSLocationListener для получения активных спутников + if (gpsLocationListener != null) { + gpsLocationListener.setSatellitesInVessel(ownVessel); + } + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + } else { +// Log.w(TAG, "GNS не совпадает с паттерном"); + } + } + + /** + * Парсит GSA сообщение (GPS DOP and Active Satellites) + * КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников + */ + private void parseGSA(String gsa) { + Log.d(TAG, "Парсим GSA: " + gsa); + Matcher matcher = GSA_PATTERN.matcher(gsa); + if (matcher.matches()) { +// Log.d(TAG, "GSA совпадает с паттерном"); + + // Подсчитываем активные спутники (непустые поля) + int activeSatellites = 0; + for (int i = 2; i <= 13; i++) { + String satId = matcher.group(i); + if (satId != null && !satId.trim().isEmpty() && !satId.equals("0")) { + activeSatellites++; + } + } + + // Получаем DOP значения - могут быть пустыми полями + double pdop = 0.0; + double hdop = 0.0; + double vdop = 0.0; + + String pdopStr = matcher.group(14); + if (pdopStr != null && !pdopStr.trim().isEmpty()) { + try { + pdop = Double.parseDouble(pdopStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить PDOP: '" + pdopStr + "', используем 0.0"); + } + } + + String hdopStr = matcher.group(15); + if (hdopStr != null && !hdopStr.trim().isEmpty()) { + try { + hdop = Double.parseDouble(hdopStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить HDOP: '" + hdopStr + "', используем 0.0"); + } + } + + String vdopStr = matcher.group(16); + if (vdopStr != null && !vdopStr.trim().isEmpty()) { + try { + vdop = Double.parseDouble(vdopStr); + } catch (NumberFormatException e) { +// Log.w(TAG, "Не удалось распарсить VDOP: '" + vdopStr + "', используем 0.0"); + } + } + +// Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f", +// activeSatellites, pdop, hdop, vdop)); + + // Обновляем информацию о спутниках + ownVessel.setActiveSatellites(activeSatellites); + ownVessel.setPdop(pdop); + ownVessel.setHdop(hdop); + ownVessel.setVdop(vdop); + + // Отправляем DOP значения в GPS Location Listener + if (gpsLocationListener != null) { + gpsLocationListener.setDOPValues(pdop, hdop, vdop); + // Синхронизируем с GPSLocationListener для получения активных спутников + gpsLocationListener.setSatellitesInVessel(ownVessel); + } + + // Уведомляем слушателя о DOP + if (listener != null) { + listener.onDOPUpdated(pdop, hdop, vdop); + listener.onVesselUpdated(ownVessel); + } + } else { + Log.w(TAG, "GSA не совпадает с паттерном"); + } + } + + /** + * Парсит AIS сообщение (Automatic Identification System) + */ + private void parseAIS(String ais) { + Matcher matcher = AIS_PATTERN.matcher(ais); + if (matcher.matches()) { + try { + int totalFragments = Integer.parseInt(matcher.group(1)); + int fragmentNumber = Integer.parseInt(matcher.group(2)); + String sequenceId = matcher.group(3); + String channel = matcher.group(4); + String payload = matcher.group(5); + int fillBits = Integer.parseInt(matcher.group(6)); + String checksum = matcher.group(7); + + Log.d(TAG, String.format("AIS: %d/%d, seq='%s', ch='%s', payload='%s', fillBits=%d, checksum='%s'", + fragmentNumber, totalFragments, sequenceId, channel, payload, fillBits, checksum)); + + // Проверяем контрольную сумму + if (!validateChecksum(ais)) { + Log.w(TAG, "AIS сообщение с неверной контрольной суммой: " + ais); + return; + } + + // Проверяем, что payload не пустой + if (payload != null && !payload.trim().isEmpty()) { + if (totalFragments == 1) { + // Одноканальное сообщение - декодируем сразу + decodeAISPayload(payload, channel.equals("A") ? 0 : 1); + } else { + // Многочастное сообщение - собираем фрагменты + // Используем номер фрагмента как sequenceId если поле пустое + String actualSequenceId = (sequenceId != null && !sequenceId.trim().isEmpty()) ? + sequenceId : String.valueOf(fragmentNumber); + collectAISFragments(actualSequenceId, fragmentNumber, totalFragments, payload, channel.equals("A") ? 0 : 1); + } + } else { + Log.w(TAG, "AIS payload пустой, пропускаем сообщение"); + } + } catch (NumberFormatException e) { + Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais); + if (listener != null) { + listener.onParseError("Ошибка парсинга AIS: " + e.getMessage()); + } + } + } else { + Log.w(TAG, "AIS сообщение не соответствует паттерну: " + ais); + Log.d(TAG, "Паттерн: " + AIS_PATTERN.pattern()); + } + } + + /** + * Декодирует AIS payload + */ + private void decodeAISPayload(String payload, int channel) { + try { + // Определяем тип AIS сообщения по первым 6 битам + String messageTypeBits = decodeAISField(payload, 0, 6); + int messageType = Integer.parseInt(messageTypeBits, 2); + + Log.d(TAG, "Декодируем AIS тип " + messageType + " на канале " + channel); + + switch (messageType) { + case 1: + case 2: + case 3: + // Position Report + decodePositionReport(payload, messageType); + break; + case 5: + // Static Data + decodeStaticData(payload); + break; + case 4: // Base Station Report + decodeBaseStationReport(payload); + break; + case 14: // Safety Related Broadcast Message + decodeSafetyBroadcast(payload); + break; + case 18: // Standard Class B Equipment Position Report + decodeClassBPositionReport(payload); + break; + case 19: // Extended Class B Equipment Position Report + decodeExtendedClassBPositionReport(payload); + break; + case 21: // Aid-to-Navigation Report + decodeAidToNavigationReport(payload); + break; + case 24: // Static Data Report + decodeStaticDataReport(payload); + break; + default: + Log.d(TAG, "Неподдерживаемый тип AIS сообщения: " + messageType); + break; + } + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования AIS payload: " + e.getMessage(), e); + } + } + + /** + * Собирает фрагменты многочастного AIS сообщения + */ + private void collectAISFragments(String sequenceId, int fragmentNumber, int totalFragments, + String payload, int channel) { + String key = sequenceId + "_" + channel; + + Log.d(TAG, String.format("Собираем AIS фраг мент: %d/%d для %s", + fragmentNumber, totalFragments, key)); + + // Очищаем старые фрагменты + cleanupOldFragments(); + + // Получаем или создаем карту фрагментов для этой последовательности + java.util.Map fragments = aisFragments.get(key); + if (fragments == null) { + fragments = new java.util.HashMap<>(); + aisFragments.put(key, fragments); + aisFragmentTimestamps.put(key, System.currentTimeMillis()); + Log.d(TAG, "Создан новый набор фрагментов для: " + key); + } + + // Добавляем фрагмент + fragments.put(fragmentNumber, payload); + Log.d(TAG, String.format("Добавлен фрагмент %d/%d для %s", + fragmentNumber, totalFragments, key)); + + // Проверяем, все ли фрагменты получены + if (fragments.size() == totalFragments) { + Log.d(TAG, "Все фрагменты получены для " + key + ", собираем сообщение"); + + // Собираем полное сообщение + StringBuilder fullPayload = new StringBuilder(); + for (int i = 1; i <= totalFragments; i++) { + String fragment = fragments.get(i); + if (fragment != null) { + fullPayload.append(fragment); + } else { + Log.w(TAG, "Отсутствует фрагмент " + i + " для " + key); + return; + } + } + + String completePayload = fullPayload.toString(); + Log.d(TAG, "Собрано полное AIS сообщение длиной " + completePayload.length() + " символов"); + + // Декодируем полное сообщение + decodeAISPayload(completePayload, channel); + + // Удаляем собранные фрагменты + aisFragments.remove(key); + aisFragmentTimestamps.remove(key); + Log.d(TAG, "Фрагменты удалены для " + key); + } else { + Log.d(TAG, String.format("Ожидаем еще %d фрагментов для %s", + totalFragments - fragments.size(), key)); + } + } + + /** + * Очищает старые AIS фрагменты + */ + private void cleanupOldFragments() { + long currentTime = System.currentTimeMillis(); + java.util.Iterator> iterator = aisFragmentTimestamps.entrySet().iterator(); + + while (iterator.hasNext()) { + java.util.Map.Entry entry = iterator.next(); + if (currentTime - entry.getValue() > AIS_FRAGMENT_TIMEOUT) { + String key = entry.getKey(); + aisFragments.remove(key); + iterator.remove(); + Log.d(TAG, "Удален устаревший AIS фрагмент: " + key); + } + } + } + + /** + * Декодирует AIS поле из битовой строки + */ + private String decodeAISField(String payload, int startBit, int length) { + StringBuilder result = new StringBuilder(); + + // Преобразуем каждый символ payload в 6-битное значение + for (int i = 0; i < payload.length(); i++) { + int ascii = payload.charAt(i); + int value; + + if (ascii >= 48 && ascii <= 87) { + value = ascii - 48; // '0'..'W' + } else if (ascii >= 88 && ascii <= 119) { + value = ascii - 56; // 'X'..'w' + } else { + throw new IllegalArgumentException("Недопустимый символ AIS payload: " + (char)ascii); + } + + // Дополняем до 6 бит слева нулями и добавляем в общую строку + String binary = String.format("%6s", Integer.toBinaryString(value)).replace(' ', '0'); + result.append(binary); + } + + String fullBinary = result.toString(); + + // Вырезаем нужный диапазон битов + if (startBit + length <= fullBinary.length()) { + return fullBinary.substring(startBit, startBit + length); + } else { + Log.w(TAG, + "AIS поле выходит за границы: startBit=" + startBit + + ", length=" + length + + ", payloadLength=" + payload.length() + + ", binaryLength=" + fullBinary.length() + ); + return fullBinary.substring(startBit, Math.min(startBit + length, fullBinary.length())); + } + } + + /** + * Декодирует AIS сообщение типа 1, 2, 3 (Position Report) + */ + private void decodePositionReport(String payload, int messageType) { + try { + Log.d(TAG, "Декодируем Position Report тип " + messageType + ", payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Navigation Status (4 бита) - бит 38 + String statusBits = decodeAISField(payload, 38, 4); + int status = Integer.parseInt(statusBits, 2); + Log.d(TAG, "Status bits: " + statusBits + " = " + status); + + // Rate of Turn (8 бит) - бит 42 + String rotBits = decodeAISField(payload, 42, 8); + int rot = Integer.parseInt(rotBits, 2); + Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rot); + + // Speed Over Ground (10 бит) - бит 50 + String speedBits = decodeAISField(payload, 50, 10); + double speed = Integer.parseInt(speedBits, 2) / 10.0; + Log.d(TAG, "Speed bits: " + speedBits + " = " + speed); + + // Position Accuracy (1 бит) - бит 60 + String accuracyBits = decodeAISField(payload, 60, 1); + int accuracy = Integer.parseInt(accuracyBits, 2); + Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy); + + // Longitude (28 бит) - бит 61 + String lonBits = decodeAISField(payload, 61, 28); + double longitude = parseAISCoordinate(lonBits, 28); + Log.d(TAG, "Longitude bits: " + lonBits + " (длина: " + lonBits.length() + ") = " + longitude); + + // Latitude (27 бит) - бит 89 + String latBits = decodeAISField(payload, 89, 27); + double latitude = parseAISCoordinate(latBits, 27); + Log.d(TAG, "Latitude bits: " + latBits + " (длина: " + latBits.length() + ") = " + latitude); + + // Course Over Ground (12 бит) - бит 116 + String courseBits = decodeAISField(payload, 116, 12); + double course = Integer.parseInt(courseBits, 2) / 10.0; + Log.d(TAG, "Course bits: " + courseBits + " = " + course); + + // True Heading (9 бит) - бит 128 + String headingBits = decodeAISField(payload, 128, 9); + double heading = Integer.parseInt(headingBits, 2); + Log.d(TAG, "Heading bits: " + headingBits + " = " + heading); + + // Time Stamp (6 бит) - бит 137 + String timestampBits = decodeAISField(payload, 137, 6); + int timestamp = Integer.parseInt(timestampBits, 2); + Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp); + + // Проверяем, что координаты в разумных пределах + if (latitude < -90 || latitude > 90) { + Log.w(TAG, "Широта вне допустимых пределов: " + latitude); + } + if (longitude < -180 || longitude > 180) { + Log.w(TAG, "Долгота вне допустимых пределов: " + longitude); + } + + Log.d(TAG, String.format("AIS Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%d, heading=%.1f", + mmsi, latitude, longitude, course, speed, status, heading)); + + // Создаем или обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.updatePosition(latitude, longitude, course, speed); + vessel.setHeading(heading); + vessel.setNavigationalStatus(getNavigationStatus(status)); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Position Report: " + e.getMessage(), e); + } + } + + /** + * Декодирует AIS сообщение типа 5 (Static Data) + */ + private void decodeStaticData(String payload) { + try { + Log.d(TAG, "Декодируем Static Data, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // AIS Version (2 бита) - бит 38 + String aisVersionBits = decodeAISField(payload, 38, 2); + int aisVersion = Integer.parseInt(aisVersionBits, 2); + Log.d(TAG, "AIS Version bits: " + aisVersionBits + " = " + aisVersion); + + // IMO Number (30 бит) - бит 40 + String imoBits = decodeAISField(payload, 40, 30); + int imo = Integer.parseInt(imoBits, 2); + Log.d(TAG, "IMO bits: " + imoBits + " = " + imo); + + // Call Sign (42 бита) - бит 70 + String callSignBits = decodeAISField(payload, 70, 42); + String callSign = decodeAISString(callSignBits); + Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'"); + + // Vessel Name (120 бит) - бит 112 + String nameBits = decodeAISField(payload, 112, 120); + String vesselName = decodeAISString(nameBits); + Log.d(TAG, "Name bits: " + nameBits + " = '" + vesselName + "'"); + + // Ship Type (8 бит) - бит 232 + String typeBits = decodeAISField(payload, 232, 8); + int vesselTypeCode = Integer.parseInt(typeBits, 2); + Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode); + + // Dimension Reference (4 бита) - бит 240 + String dimRefABits = decodeAISField(payload, 240, 4); + String dimRefBBits = decodeAISField(payload, 244, 4); + String dimRefCBits = decodeAISField(payload, 248, 4); + String dimRefDBits = decodeAISField(payload, 252, 4); + + int dimRefA = Integer.parseInt(dimRefABits, 2); + int dimRefB = Integer.parseInt(dimRefBBits, 2); + int dimRefC = Integer.parseInt(dimRefCBits, 2); + int dimRefD = Integer.parseInt(dimRefDBits, 2); + + Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD); + + // Vessel Dimensions (30 бит) - бит 256 + String lengthBits = decodeAISField(payload, 256, 10); + String widthBits = decodeAISField(payload, 266, 10); + String draftBits = decodeAISField(payload, 276, 8); + + double length = Integer.parseInt(lengthBits, 2); + double width = Integer.parseInt(widthBits, 2); + double draft = Integer.parseInt(draftBits, 2) / 10.0; + + Log.d(TAG, "Dimensions - Length bits: " + lengthBits + " = " + length); + Log.d(TAG, "Dimensions - Width bits: " + widthBits + " = " + width); + Log.d(TAG, "Dimensions - Draft bits: " + draftBits + " = " + draft); + + // ETA (20 бит) - бит 294 + String etaBits = decodeAISField(payload, 294, 20); + int eta = Integer.parseInt(etaBits, 2); + Log.d(TAG, "ETA bits: " + etaBits + " = " + eta); + + // Destination (120 бит) - бит 314 + String destBits = decodeAISField(payload, 314, 120); + String destination = decodeAISString(destBits); + Log.d(TAG, "Destination bits: " + destBits + " = '" + destination + "'"); + + Log.d(TAG, String.format("AIS Static: MMSI=%d, IMO=%d, name='%s', callSign='%s', type=%d, L=%.1f, W=%.1f, D=%.1f, ETA=%d, dest='%s'", + mmsi, imo, vesselName, callSign, vesselTypeCode, length, width, draft, eta, destination)); + + // Обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.setVesselName(vesselName); + vessel.setCallSign(callSign); + vessel.setImo(imo); + vessel.setVesselType(getVesselType(vesselTypeCode)); + vessel.setLength(length); + vessel.setWidth(width); + vessel.setDraft(draft); + vessel.setDestination(destination); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Static Data: " + e.getMessage(), e); + } + } + + /** + * Парсит AIS координаты + */ + private double parseAISCoordinate(String bits, int bitLength) { + // Проверяем знаковый бит + boolean isNegative = bits.charAt(0) == '1'; + + // Преобразуем в беззнаковое число + long value = Long.parseLong(bits, 2); + + if (bitLength == 27) { + // Широта: 27 бит, диапазон -90 до +90 + if (isNegative) { + // Для отрицательных чисел применяем дополнение до двух + value = value - (1L << 27); + } + return value / 600000.0; + } else { + // Долгота: 28 бит, диапазон -180 до +180 + if (isNegative) { + // Для отрицательных чисел применяем дополнение до двух + value = value - (1L << 28); + } + return value / 600000.0; + } + } + + /** + * Декодирует AIS строку + */ + private String decodeAISString(String bits) { + StringBuilder result = new StringBuilder(); + Log.d(TAG, "Декодируем AIS строку из битов: " + bits + " (длина: " + bits.length() + ")"); + + for (int i = 0; i < bits.length(); i += 6) { + if (i + 6 <= bits.length()) { + String charBits = bits.substring(i, i + 6); + int value = Integer.parseInt(charBits, 2); + + if (value == 0) { + Log.d(TAG, "Найден конец строки (0)"); + break; // Конец строки + } + + char decodedChar; + if (value >= 1 && value <= 26) { + decodedChar = (char)('A' + value - 1); + } else if (value >= 27 && value <= 52) { + decodedChar = (char)('a' + value - 27); + } else if (value >= 53 && value <= 62) { + decodedChar = (char)('0' + value - 53); + } else if (value == 63) { + decodedChar = ' '; + } else if (value == 0) { + decodedChar = '@'; // Специальный символ + } else { + decodedChar = '?'; // Неизвестный символ + Log.w(TAG, "Неизвестное значение AIS символа: " + value); + } + + result.append(decodedChar); + Log.d(TAG, "Декодирован символ: " + charBits + " (" + value + ") -> '" + decodedChar + "'"); + } + } + + String resultStr = result.toString().trim(); + Log.d(TAG, "Результат декодирования строки: '" + resultStr + "'"); + return resultStr; + } + + /** + * Получает навигационный статус по коду + */ + private String getNavigationStatus(int status) { + switch (status) { + case 0: return "Under way using engine"; + case 1: return "At anchor"; + case 2: return "Not under command"; + case 3: return "Restricted manoeuvrability"; + case 4: return "Constrained by her draught"; + case 5: return "Moored"; + case 6: return "Aground"; + case 7: return "Engaged in fishing"; + case 8: return "Under way sailing"; + case 9: return "Reserved"; + case 10: return "Reserved"; + case 11: return "Reserved"; + case 12: return "Reserved"; + case 13: return "Reserved"; + case 14: return "AIS-SART"; + case 15: return "Not defined"; + default: return "Unknown"; + } + } + + /** + * Получает тип судна по коду + */ + private String getVesselType(int typeCode) { + if (typeCode >= 20 && typeCode <= 29) return "Wing in ground"; + if (typeCode >= 30 && typeCode <= 39) return "Fishing"; + if (typeCode >= 40 && typeCode <= 49) return "Towing"; + if (typeCode >= 50 && typeCode <= 59) return "Dredging"; + if (typeCode >= 60 && typeCode <= 69) return "Diving"; + if (typeCode >= 70 && typeCode <= 79) return "Military"; + if (typeCode >= 80 && typeCode <= 89) return "Pleasure"; + if (typeCode >= 90 && typeCode <= 99) return "High speed"; + if (typeCode >= 100 && typeCode <= 109) return "Pilot vessel"; + if (typeCode >= 110 && typeCode <= 119) return "SAR"; + if (typeCode >= 120 && typeCode <= 129) return "Tug"; + if (typeCode >= 130 && typeCode <= 139) return "Port tender"; + if (typeCode >= 140 && typeCode <= 149) return "Anti-pollution"; + if (typeCode >= 150 && typeCode <= 159) return "Law enforce"; + if (typeCode >= 160 && typeCode <= 169) return "Spare"; + if (typeCode >= 170 && typeCode <= 179) return "Medical"; + if (typeCode >= 180 && typeCode <= 189) return "Special craft"; + if (typeCode >= 190 && typeCode <= 199) return "Passenger"; + if (typeCode >= 200 && typeCode <= 209) return "Cargo"; + if (typeCode >= 210 && typeCode <= 219) return "Tanker"; + if (typeCode >= 220 && typeCode <= 229) return "Other"; + return "Unknown"; + } + + /** + * Находит существующее AIS судно или создает новое + */ + private AISVessel findOrCreateAISVessel(String mmsi) { + for (AISVessel vessel : aisVessels) { + if (mmsi.equals(vessel.getMmsi())) { + return vessel; + } + } + + // Создаем новое судно + AISVessel newVessel = new AISVessel(mmsi); + aisVessels.add(newVessel); + Log.d(TAG, "Создано новое AIS судно: " + mmsi); + return newVessel; + } + + /** + * Очищает устаревшие AIS суда (данные старше 10 минут) + */ + public void cleanupStaleAISVessels() { + java.util.Iterator iterator = aisVessels.iterator(); + int removedCount = 0; + + while (iterator.hasNext()) { + AISVessel vessel = iterator.next(); + if (vessel.isDataStale()) { + iterator.remove(); + removedCount++; + Log.d(TAG, "Удалено устаревшее AIS судно: " + vessel.getMmsi()); + } + } + + if (removedCount > 0) { + Log.i(TAG, "Удалено " + removedCount + " устаревших AIS судов"); + } + } + + /** + * Получает количество активных AIS судов + */ + public int getActiveAISVesselCount() { + cleanupStaleAISVessels(); + return aisVessels.size(); + } + + /** + * Получает AIS судно по MMSI + */ + public AISVessel getAISVesselByMMSI(String mmsi) { + for (AISVessel vessel : aisVessels) { + if (mmsi.equals(vessel.getMmsi())) { + return vessel; + } + } + return null; + } + + /** + * Обновляет статус активности AIS судов + */ + public void updateAISVesselActivity() { + long currentTime = System.currentTimeMillis(); + for (AISVessel vessel : aisVessels) { + // Считаем судно активным, если данные получены менее 5 минут назад + boolean isActive = (currentTime - vessel.getLastUpdate().toInstant(java.time.ZoneOffset.UTC).toEpochMilli()) < 300000; + vessel.setActive(isActive); + } + } + + /** + * Парсит координаты из NMEA формата + */ + private double parseCoordinate(String coordinate, boolean isPositive) { + // Проверяем, что координата не пустая + if (coordinate == null || coordinate.trim().isEmpty()) { + return 0.0; + } + + try { + double value = Double.parseDouble(coordinate); + int degrees = (int) (value / 100); + double minutes = value - (degrees * 100); + double result = degrees + (minutes / 60.0); + return isPositive ? result : -result; + } catch (NumberFormatException e) { + Log.w(TAG, "Ошибка парсинга координаты: " + coordinate + ", ошибка: " + e.getMessage()); + return 0.0; + } + } + + /** + * Проверяет контрольную сумму NMEA сообщения + */ + public boolean validateChecksum(String nmeaSentence) { + if (nmeaSentence == null || !nmeaSentence.contains("*")) { + return false; + } + + int asteriskIndex = nmeaSentence.indexOf('*'); + String sentence = nmeaSentence.substring(1, asteriskIndex); + String checksum = nmeaSentence.substring(asteriskIndex + 1); + + int calculatedChecksum = 0; + for (char c : sentence.toCharArray()) { + calculatedChecksum ^= c; + } + + String hexChecksum = String.format("%02X", calculatedChecksum); + return hexChecksum.equals(checksum); + } + + public Vessel getOwnVessel() { + return ownVessel; + } + + public List getAISVessels() { + return new ArrayList<>(aisVessels); + } + + /** + * Получает количество спутников GPS + */ + public int getGPSSatellites() { + return gpsSatellites; + } + + /** + * Получает количество спутников GLONASS + */ + public int getGLONASSSatellites() { + return glonassSatellites; + } + + /** + * Получает количество спутников Galileo + */ + public int getGalileoSatellites() { + return galileoSatellites; + } + + /** + * Получает общее количество спутников всех систем + */ + public int getTotalSatellites() { + return gpsSatellites + glonassSatellites + galileoSatellites; + } + + /** + * Сбрасывает счетчики спутников + */ + public void resetSatelliteCounters() { + gpsSatellites = 0; + glonassSatellites = 0; + galileoSatellites = 0; + ownVessel.setSatellites(0); + Log.d(TAG, "Счетчики спутников сброшены"); + } + + /** + * Синхронизирует данные о спутниках с GPSLocationListener + */ + public void syncSatelliteData() { + if (gpsLocationListener != null) { + gpsLocationListener.setSatellitesInVessel(ownVessel); + } + } + + /** + * Получает текущее состояние объекта Vessel + */ + public String getVesselStatus() { + return String.format("Vessel: satellites=%d, activeSatellites=%d, GPS=%d, GLONASS=%d, Galileo=%d", + ownVessel.getSatellites(), ownVessel.getActiveSatellites(), + gpsSatellites, glonassSatellites, galileoSatellites); + } + + /** + * Декодирует AIS сообщение типа 4 (Base Station Report) + */ + private void decodeBaseStationReport(String payload) { + try { + Log.d(TAG, "Декодируем Base Station Report, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Year (14 бит) - бит 38 + String yearBits = decodeAISField(payload, 38, 14); + int year = Integer.parseInt(yearBits, 2); + Log.d(TAG, "Year bits: " + yearBits + " = " + year); + + // Month (4 бита) - бит 52 + String monthBits = decodeAISField(payload, 52, 4); + int month = Integer.parseInt(monthBits, 2); + Log.d(TAG, "Month bits: " + monthBits + " = " + month); + + // Day (5 бит) - бит 56 + String dayBits = decodeAISField(payload, 56, 5); + int day = Integer.parseInt(dayBits, 2); + Log.d(TAG, "Day bits: " + dayBits + " = " + day); + + // Hour (5 бит) - бит 61 + String hourBits = decodeAISField(payload, 61, 5); + int hour = Integer.parseInt(hourBits, 2); + Log.d(TAG, "Hour bits: " + hourBits + " = " + hour); + + // Minute (6 бит) - бит 66 + String minuteBits = decodeAISField(payload, 66, 6); + int minute = Integer.parseInt(minuteBits, 2); + Log.d(TAG, "Minute bits: " + minuteBits + " = " + minute); + + // Second (6 бит) - бит 72 + String secondBits = decodeAISField(payload, 72, 6); + int second = Integer.parseInt(secondBits, 2); + Log.d(TAG, "Second bits: " + secondBits + " = " + second); + + // Position Accuracy (1 бит) - бит 78 + String accuracyBits = decodeAISField(payload, 78, 1); + int accuracy = Integer.parseInt(accuracyBits, 2); + Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy); + + // Longitude (28 бит) - бит 79 + String lonBits = decodeAISField(payload, 79, 28); + double longitude = parseAISCoordinate(lonBits, 28); + Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude); + + // Latitude (27 бит) - бит 107 + String latBits = decodeAISField(payload, 107, 27); + double latitude = parseAISCoordinate(latBits, 27); + Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude); + + // EPFD Type (4 бита) - бит 134 + String epfdBits = decodeAISField(payload, 134, 4); + int epfdType = Integer.parseInt(epfdBits, 2); + Log.d(TAG, "EPFD Type bits: " + epfdBits + " = " + epfdType); + + Log.d(TAG, String.format("AIS Base Station: MMSI=%d, date=%04d-%02d-%02d %02d:%02d:%02d, lat=%.6f, lon=%.6f, accuracy=%d, epfd=%d", + mmsi, year, month, day, hour, minute, second, latitude, longitude, accuracy, epfdType)); + + // Создаем или обновляем AIS судно (базовая станция) + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.updatePosition(latitude, longitude, 0.0, 0.0); + vessel.setPositionAccuracy(accuracy == 1); + vessel.setVesselClass("Base Station"); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Base Station Report: " + e.getMessage(), e); + } + } + + /** + * Декодирует AIS сообщение типа 14 (Safety Related Broadcast Message) + */ + private void decodeSafetyBroadcast(String payload) { + try { + Log.d(TAG, "Декодируем Safety Broadcast, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Spare (2 бита) - бит 38 + String spareBits = decodeAISField(payload, 38, 2); + int spare = Integer.parseInt(spareBits, 2); + Log.d(TAG, "Spare bits: " + spareBits + " = " + spare); + + // Text (120 бит) - бит 40 + String textBits = decodeAISField(payload, 40, 120); + String safetyText = decodeAISString(textBits); + Log.d(TAG, "Safety Text bits: " + textBits + " = '" + safetyText + "'"); + + Log.d(TAG, String.format("AIS Safety Broadcast: MMSI=%d, text='%s'", mmsi, safetyText)); + + // Создаем или обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.setLastSafetyMessage(safetyText); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Safety Broadcast: " + e.getMessage(), e); + } + } + + /** + * Декодирует AIS сообщение типа 18 (Standard Class B Equipment Position Report) + */ + private void decodeClassBPositionReport(String payload) { + try { + Log.d(TAG, "Декодируем Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Speed Over Ground (10 бит) - бит 46 + String speedBits = decodeAISField(payload, 46, 10); + double speed = Integer.parseInt(speedBits, 2) / 10.0; + Log.d(TAG, "Speed bits: " + speedBits + " = " + speed); + + // Position Accuracy (1 бит) - бит 56 + String accuracyBits = decodeAISField(payload, 56, 1); + int accuracy = Integer.parseInt(accuracyBits, 2); + Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy); + + // Longitude (28 бит) - бит 57 + String lonBits = decodeAISField(payload, 57, 28); + double longitude = parseAISCoordinate(lonBits, 28); + Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude); + + // Latitude (27 бит) - бит 85 + String latBits = decodeAISField(payload, 85, 27); + double latitude = parseAISCoordinate(latBits, 27); + Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude); + + // Course Over Ground (12 бит) - бит 112 + String courseBits = decodeAISField(payload, 112, 12); + double course = Integer.parseInt(courseBits, 2) / 10.0; + Log.d(TAG, "Course bits: " + courseBits + " = " + course); + + // True Heading (9 бит) - бит 124 + String headingBits = decodeAISField(payload, 124, 9); + double heading = Integer.parseInt(headingBits, 2); + Log.d(TAG, "Heading bits: " + headingBits + " = " + heading); + + // Time Stamp (6 бит) - бит 133 + String timestampBits = decodeAISField(payload, 133, 6); + int timestamp = Integer.parseInt(timestampBits, 2); + Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp); + + // Regional Reserved (2 бита) - бит 139 + String regionalBits = decodeAISField(payload, 139, 2); + int regional = Integer.parseInt(regionalBits, 2); + Log.d(TAG, "Regional bits: " + regionalBits + " = " + regional); + + // Spare (3 бита) - бит 141 + String spareBits = decodeAISField(payload, 141, 3); + int spare = Integer.parseInt(spareBits, 2); + Log.d(TAG, "Spare bits: " + spareBits + " = " + spare); + + Log.d(TAG, String.format("AIS Class B Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f", + mmsi, latitude, longitude, course, speed, heading)); + + // Создаем или обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.updatePosition(latitude, longitude, course, speed); + vessel.setHeading(heading); + vessel.setPositionAccuracy(accuracy == 1); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + vessel.setVesselClass("Class B"); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Class B Position Report: " + e.getMessage(), e); + } + } + + /** + * Декодирует AIS сообщение типа 19 (Extended Class B Equipment Position Report) + */ + private void decodeExtendedClassBPositionReport(String payload) { + try { + Log.d(TAG, "Декодируем Extended Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Speed Over Ground (10 бит) - бит 46 + String speedBits = decodeAISField(payload, 46, 10); + double speed = Integer.parseInt(speedBits, 2) / 10.0; + Log.d(TAG, "Speed bits: " + speedBits + " = " + speed); + + // Position Accuracy (1 бит) - бит 56 + String accuracyBits = decodeAISField(payload, 56, 1); + int accuracy = Integer.parseInt(accuracyBits, 2); + Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy); + + // Longitude (28 бит) - бит 57 + String lonBits = decodeAISField(payload, 57, 28); + double longitude = parseAISCoordinate(lonBits, 28); + Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude); + + // Latitude (27 бит) - бит 85 + String latBits = decodeAISField(payload, 85, 27); + double latitude = parseAISCoordinate(latBits, 27); + Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude); + + // Course Over Ground (12 бит) - бит 112 + String courseBits = decodeAISField(payload, 112, 12); + double course = Integer.parseInt(courseBits, 2) / 10.0; + Log.d(TAG, "Course bits: " + courseBits + " = " + course); + + // True Heading (9 бит) - бит 124 + String headingBits = decodeAISField(payload, 124, 9); + double heading = Integer.parseInt(headingBits, 2); + Log.d(TAG, "Heading bits: " + headingBits + " = " + heading); + + // Time Stamp (6 бит) - бит 133 + String timestampBits = decodeAISField(payload, 133, 6); + int timestamp = Integer.parseInt(timestampBits, 2); + Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp); + + // Regional Reserved (4 бита) - бит 139 + String regionalBits = decodeAISField(payload, 139, 4); + int regional = Integer.parseInt(regionalBits, 2); + Log.d(TAG, "Regional bits: " + regionalBits + " = " + regional); + + // Vessel Name (120 бит) - бит 143 + String nameBits = decodeAISField(payload, 143, 120); + String vesselName = decodeAISString(nameBits); + Log.d(TAG, "Name bits: " + nameBits + " = '" + vesselName + "'"); + + // Ship Type (8 бит) - бит 263 + String typeBits = decodeAISField(payload, 263, 8); + int vesselTypeCode = Integer.parseInt(typeBits, 2); + Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode); + + // Dimension Reference (4 бита) - бит 271 + String dimRefABits = decodeAISField(payload, 271, 4); + String dimRefBBits = decodeAISField(payload, 275, 4); + String dimRefCBits = decodeAISField(payload, 279, 4); + String dimRefDBits = decodeAISField(payload, 283, 4); + + int dimRefA = Integer.parseInt(dimRefABits, 2); + int dimRefB = Integer.parseInt(dimRefBBits, 2); + int dimRefC = Integer.parseInt(dimRefCBits, 2); + int dimRefD = Integer.parseInt(dimRefDBits, 2); + + // Vessel Dimensions (30 бит) - бит 287 + String lengthBits = decodeAISField(payload, 287, 10); + String widthBits = decodeAISField(payload, 297, 10); + String draftBits = decodeAISField(payload, 307, 8); + + double length = Integer.parseInt(lengthBits, 2); + double width = Integer.parseInt(widthBits, 2); + double draft = Integer.parseInt(draftBits, 2) / 10.0; + + Log.d(TAG, String.format("AIS Extended Class B: MMSI=%d, name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, type=%d, L=%.1f, W=%.1f, D=%.1f", + mmsi, vesselName, latitude, longitude, course, speed, vesselTypeCode, length, width, draft)); + + // Создаем или обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.updatePosition(latitude, longitude, course, speed); + vessel.setHeading(heading); + vessel.setPositionAccuracy(accuracy == 1); + vessel.setVesselName(vesselName); + vessel.setVesselType(getVesselType(vesselTypeCode)); + vessel.setLength(length); + vessel.setWidth(width); + vessel.setDraft(draft); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + vessel.setVesselClass("Extended Class B"); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Extended Class B Position Report: " + e.getMessage(), e); + } + } + + /** + * Декодирует AIS сообщение типа 21 (Aid-to-Navigation Report) + */ + private void decodeAidToNavigationReport(String payload) { + try { + Log.d(TAG, "Декодируем Aid-to-Navigation Report, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Aid Type (5 бит) - бит 38 + String aidTypeBits = decodeAISField(payload, 38, 5); + int aidType = Integer.parseInt(aidTypeBits, 2); + Log.d(TAG, "Aid Type bits: " + aidTypeBits + " = " + aidType); + + // Name (120 бит) - бит 43 + String nameBits = decodeAISField(payload, 43, 120); + String aidName = decodeAISString(nameBits); + Log.d(TAG, "Name bits: " + nameBits + " = '" + aidName + "'"); + + // Position Accuracy (1 бит) - бит 163 + String accuracyBits = decodeAISField(payload, 163, 1); + int accuracy = Integer.parseInt(accuracyBits, 2); + Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy); + + // Longitude (28 бит) - бит 164 + String lonBits = decodeAISField(payload, 164, 28); + double longitude = parseAISCoordinate(lonBits, 28); + Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude); + + // Latitude (27 бит) - бит 192 + String latBits = decodeAISField(payload, 192, 27); + double latitude = parseAISCoordinate(latBits, 27); + Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude); + + // Dimension Reference (4 бита) - бит 219 + String dimRefABits = decodeAISField(payload, 219, 4); + String dimRefBBits = decodeAISField(payload, 223, 4); + String dimRefCBits = decodeAISField(payload, 227, 4); + String dimRefDBits = decodeAISField(payload, 231, 4); + + int dimRefA = Integer.parseInt(dimRefABits, 2); + int dimRefB = Integer.parseInt(dimRefBBits, 2); + int dimRefC = Integer.parseInt(dimRefCBits, 2); + int dimRefD = Integer.parseInt(dimRefDBits, 2); + + // Vessel Dimensions (30 бит) - бит 235 + String lengthBits = decodeAISField(payload, 235, 10); + String widthBits = decodeAISField(payload, 245, 10); + String draftBits = decodeAISField(payload, 255, 8); + + double length = Integer.parseInt(lengthBits, 2); + double width = Integer.parseInt(widthBits, 2); + double draft = Integer.parseInt(draftBits, 2) / 10.0; + + Log.d(TAG, String.format("AIS Aid-to-Navigation: MMSI=%d, type=%d, name='%s', lat=%.6f, lon=%.6f, L=%.1f, W=%.1f, D=%.1f", + mmsi, aidType, aidName, latitude, longitude, length, width, draft)); + + // Создаем или обновляем AIS судно (навигационный знак) + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.updatePosition(latitude, longitude, 0.0, 0.0); + vessel.setPositionAccuracy(accuracy == 1); + vessel.setVesselName(aidName); + vessel.setVesselType("Aid-to-Navigation"); + vessel.setLength(length); + vessel.setWidth(width); + vessel.setDraft(draft); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + vessel.setVesselClass("Navigation Aid"); + + // Уведомляем слушателя + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Aid-to-Navigation Report: " + e.getMessage(), e); + } + } + + /** + * Декодирует AIS сообщение типа 24 (Static Data Report) + */ + private void decodeStaticDataReport(String payload) { + try { + Log.d(TAG, "Декодируем Static Data Report, payload: " + payload + " (длина: " + payload.length() + ")"); + + // MMSI (30 бит) - начинается с бита 8 + String mmsiBits = decodeAISField(payload, 8, 30); + int mmsi = Integer.parseInt(mmsiBits, 2); + Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi); + + // Part Number (2 бита) - бит 38 + String partBits = decodeAISField(payload, 38, 2); + int partNumber = Integer.parseInt(partBits, 2); + Log.d(TAG, "Part Number bits: " + partBits + " = " + partNumber); + + if (partNumber == 0) { + // Part A: Vessel Name + String nameBits = decodeAISField(payload, 40, 120); + String vesselName = decodeAISString(nameBits); + Log.d(TAG, "Vessel Name bits: " + nameBits + " = '" + vesselName + "'"); + + Log.d(TAG, String.format("AIS Static Data Part A: MMSI=%d, name='%s'", mmsi, vesselName)); + + // Обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.setVesselName(vesselName); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + + } else if (partNumber == 1) { + // Part B: Vessel Type, Dimensions, etc. + String typeBits = decodeAISField(payload, 40, 8); + int vesselTypeCode = Integer.parseInt(typeBits, 2); + Log.d(TAG, "Vessel Type bits: " + typeBits + " = " + vesselTypeCode); + + // Vendor ID (42 бита) - бит 48 + String vendorBits = decodeAISField(payload, 48, 42); + String vendorId = decodeAISString(vendorBits); + Log.d(TAG, "Vendor ID bits: " + vendorBits + " = '" + vendorId + "'"); + + // Call Sign (42 бита) - бит 90 + String callSignBits = decodeAISField(payload, 90, 42); + String callSign = decodeAISString(callSignBits); + Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'"); + + // Dimension Reference (4 бита) - бит 132 + String dimRefABits = decodeAISField(payload, 132, 4); + String dimRefBBits = decodeAISField(payload, 136, 4); + String dimRefCBits = decodeAISField(payload, 140, 4); + String dimRefDBits = decodeAISField(payload, 144, 4); + + int dimRefA = Integer.parseInt(dimRefABits, 2); + int dimRefB = Integer.parseInt(dimRefBBits, 2); + int dimRefC = Integer.parseInt(dimRefCBits, 2); + int dimRefD = Integer.parseInt(dimRefDBits, 2); + + // Vessel Dimensions (30 бит) - бит 148 + String lengthBits = decodeAISField(payload, 148, 10); + String widthBits = decodeAISField(payload, 158, 10); + String draftBits = decodeAISField(payload, 168, 8); + + double length = Integer.parseInt(lengthBits, 2); + double width = Integer.parseInt(widthBits, 2); + double draft = Integer.parseInt(draftBits, 2) / 10.0; + + Log.d(TAG, String.format("AIS Static Data Part B: MMSI=%d, type=%d, vendor='%s', callSign='%s', L=%.1f, W=%.1f, D=%.1f", + mmsi, vesselTypeCode, vendorId, callSign, length, width, draft)); + + // Обновляем AIS судно + AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); + vessel.setVesselType(getVesselType(vesselTypeCode)); + vessel.setVendorId(vendorId); + vessel.setCallSign(callSign); + vessel.setLength(length); + vessel.setWidth(width); + vessel.setDraft(draft); + vessel.setLastUpdate(java.time.LocalDateTime.now()); + + if (listener != null) { + listener.onAISVesselUpdated(vessel); + } + } + + } catch (Exception e) { + Log.e(TAG, "Ошибка декодирования Static Data Report: " + e.getMessage(), e); + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/UDPListener.java b/app/src/main/java/com/grigowashere/aismap/controllers/UDPListener.java new file mode 100644 index 0000000..5cf3d9c --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/controllers/UDPListener.java @@ -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(); + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java new file mode 100644 index 0000000..14b60c4 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java @@ -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 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; + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java b/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java new file mode 100644 index 0000000..86716b7 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/MapInterface.java @@ -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); + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java new file mode 100644 index 0000000..fa01bfa --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java @@ -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 aisMarkers; + private Map aisVessels; // Храним ссылки на AISVessel объекты + private com.yandex.mapkit.map.PlacemarkMapObject ownVesselMarker; + private Vessel ownVessel; // Храним ссылку на наше судно + + // Флаги для отслеживания состояния обработчиков + private boolean ownVesselClickListenerSet = false; + private Map 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 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 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; + } + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/models/AISVessel.java b/app/src/main/java/com/grigowashere/aismap/models/AISVessel.java new file mode 100644 index 0000000..0fcd8f7 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/models/AISVessel.java @@ -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 + + '}'; + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/models/Vessel.java b/app/src/main/java/com/grigowashere/aismap/models/Vessel.java new file mode 100644 index 0000000..860be78 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/models/Vessel.java @@ -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() + + '}'; + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/sensors/CompassSensor.java b/app/src/main/java/com/grigowashere/aismap/sensors/CompassSensor.java new file mode 100644 index 0000000..a73bffb --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/sensors/CompassSensor.java @@ -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; + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java b/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java new file mode 100644 index 0000000..61979f0 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/utils/GeoUtils.java @@ -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; + } + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java b/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java new file mode 100644 index 0000000..52e3835 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java @@ -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); +} diff --git a/app/src/main/java/com/grigowashere/aismap/view/CompassView.java b/app/src/main/java/com/grigowashere/aismap/view/CompassView.java new file mode 100644 index 0000000..b7855ad --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/view/CompassView.java @@ -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 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 vessels) { + this.nearbyVessels = vessels; + invalidate(); + } + + public void setOurVessel(Vessel ourVessel) { + this.ourVessel = ourVessel; + invalidate(); + } +} diff --git a/app/src/main/res/drawable/arrowship.xml b/app/src/main/res/drawable/arrowship.xml new file mode 100644 index 0000000..0905e59 --- /dev/null +++ b/app/src/main/res/drawable/arrowship.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..2f0bfcf --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,101 @@ + + + + + + + + + +