Initial commit: AIS Map Android application

This commit is contained in:
ОС Программист
2025-09-02 15:58:16 +03:00
commit 629b403dd2
78 changed files with 9209 additions and 0 deletions
+15
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidProjectSystem">
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
</component>
</project>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="21" />
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetSelector">
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
</SelectionState>
</selectionStates>
</component>
</project>
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="CHOOSE_PER_TEST" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectMigrations">
<option name="MigrateToGradleLocalJavaHome">
<set>
<option value="$PROJECT_DIR$" />
</set>
</option>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.intellij.execution.junit.AbstractAllInDirectoryConfigurationProducer" />
<option value="com.intellij.execution.junit.AllInPackageConfigurationProducer" />
<option value="com.intellij.execution.junit.PatternConfigurationProducer" />
<option value="com.intellij.execution.junit.TestInClassConfigurationProducer" />
<option value="com.intellij.execution.junit.UniqueIdConfigurationProducer" />
<option value="com.intellij.execution.junit.testDiscovery.JUnitTestDiscoveryConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinJUnitRunConfigurationProducer" />
<option value="org.jetbrains.kotlin.idea.junit.KotlinPatternConfigurationProducer" />
</set>
</option>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="T:/sources/repository" vcs="Git" />
</component>
</project>
+132
View File
@@ -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. Добавить логирование и обработку ошибок
### Кастомные поля
- Поддержка региональных расширений
- Обработка производитель-специфичных данных
- Гибкая система метаданных
+142
View File
@@ -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 системы и богатство морских навигационных данных.
+148
View File
@@ -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.
+157
View File
@@ -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
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
### API ключи
- Яндекс.Карты: `9ae1917c-2049-4927-9d1e-29dd0d3e8ebc`
## Использование
1. Запустите приложение
2. Предоставьте разрешения на GPS
3. Включите GPS слушатель
4. При необходимости включите UDP слушатель
5. Используйте кнопки управления для навигации
## Планы развития
- [ ] Поддержка BLE устройств
- [ ] Расширенный декодер AIS
- [ ] Сохранение истории маршрутов
- [ ] Экспорт данных
- [ ] Настройки отображения
- [ ] Многоязычная поддержка
## Лицензия
Проект разработан для образовательных целей.
+1
View File
@@ -0,0 +1 @@
/build
+53
View File
@@ -0,0 +1,53 @@
plugins {
id 'com.android.application'
}
android {
namespace 'com.grigowashere.aismap'
compileSdk 35
defaultConfig {
applicationId "com.grigowashere.aismap"
minSdk 30
targetSdk 35
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// Яндекс.Карты
implementation 'com.yandex.android:maps.mobile:4.4.0-full'
implementation group: 'org.mapsforge', name: 'mapsforge-map-android', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-themes', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-map', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-map-reader', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.25.0'
// Тестирование
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,26 @@
package com.grigowashere.aismap;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.grigowashere.aismap", appContext.getPackageName());
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Разрешения для GPS -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!-- Разрешения для интернета (для Яндекс.Карт) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- Разрешения для UDP -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Разрешения для записи в файл (для логирования) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<!-- Дополнительные разрешения для GPS -->
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-feature android:name="android.hardware.location" android:required="false" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AISMap"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/Theme.AISMap">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Мета-данные для Яндекс.Карт -->
<meta-data
android:name="com.yandex.mapkit.ApiKey"
android:value="9ae1917c-2049-4927-9d1e-29dd0d3e8ebc" />
</application>
</manifest>
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
import: https://mapzen.com/carto/bubble-wrap-style/9/bubble-wrap-style.zip
sources:
osm:
type: MVT
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png
max_zoom: 19
layers:
earth:
data: { source: osm }
draw:
background:
color: '#f8f4f0'
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,615 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.location.LocationManager;
import android.location.OnNmeaMessageListener;
import android.location.LocationListener;
import android.os.Build;
import android.util.Log;
import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.GpsStatus.Listener;
import android.os.Looper;
import android.os.Handler;
/**
* Контроллер для Android NMEA Listener
* Использует встроенный GPS для получения NMEA сообщений
*/
public class AndroidNMEAListener implements OnNmeaMessageListener {
private static final String TAG = "AndroidNMEAListener";
private Context context;
private LocationManager locationManager;
private NMEAMessageCallback callback;
private boolean isListening;
private int satelliteCount;
private LocationListener locationListener;
private GpsStatus.Listener gpsStatusListener;
private GnssStatus.Callback gnssStatusCallback;
private Handler activationHandler;
private boolean hasReceivedNMEA;
public interface NMEAMessageCallback {
void onNMEAMessage(String message, long timestamp);
void onGPSStatusChanged(int status);
void onError(String error);
}
public AndroidNMEAListener(Context context) {
this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
this.isListening = false;
this.activationHandler = new Handler(Looper.getMainLooper());
this.hasReceivedNMEA = false;
}
public void setCallback(NMEAMessageCallback callback) {
this.callback = callback;
}
/**
* Запускает прослушивание NMEA сообщений
*/
public boolean startListening() {
if (isListening) {
//Log.w(TAG, "NMEA слушатель уже запущен");
return true;
}
if (locationManager == null) {
//Log.e(TAG, "LocationManager недоступен");
if (callback != null) {
callback.onError("LocationManager недоступен");
}
return false;
}
// Проверяем все доступные провайдеры
//Log.i(TAG, "=== ДИАГНОСТИКА GPS ===");
//Log.i(TAG, "Доступные провайдеры:");
for (String provider : locationManager.getAllProviders()) {
boolean enabled = locationManager.isProviderEnabled(provider);
//Log.i(TAG, " " + provider + ": " + (enabled ? "включен" : "отключен"));
}
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
//Log.w(TAG, "GPS провайдер отключен");
if (callback != null) {
callback.onError("GPS провайдер отключен");
}
return false;
}
// Проверяем разрешения
if (context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
//Log.e(TAG, "Нет разрешения ACCESS_FINE_LOCATION");
if (callback != null) {
callback.onError("Нет разрешения ACCESS_FINE_LOCATION");
}
return false;
}
//Log.i(TAG, "Разрешения GPS получены");
try {
//Log.i(TAG, "=== РЕГИСТРАЦИЯ СЛУШАТЕЛЕЙ ===");
// Регистрируем NMEA слушатель с минимальным интервалом
locationManager.addNmeaListener(this, new Handler(Looper.getMainLooper()));
//Log.i(TAG, "✅ NMEA слушатель зарегистрирован");
// Регистрируем GPS статус слушатель (для старых версий Android)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
gpsStatusListener = new GpsStatus.Listener() {
@Override
public void onGpsStatusChanged(int event) {
String eventName = getGpsEventName(event);
//Log.d(TAG, "GPS статус изменился: " + event + " (" + eventName + ")");
if (callback != null) {
callback.onGPSStatusChanged(event);
}
// При получении первого фиксирования запрашиваем обновления
if (event == GpsStatus.GPS_EVENT_FIRST_FIX) {
//Log.i(TAG, "🎯 Получено первое GPS фиксирование, активируем NMEA");
requestLocationUpdates();
}
}
};
locationManager.addGpsStatusListener(gpsStatusListener);
//Log.i(TAG, "✅ GPS статус слушатель зарегистрирован (старый API)");
}
// Регистрируем GNSS статус callback (для новых версий Android)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
gnssStatusCallback = new GnssStatus.Callback() {
@Override
public void onStarted() {
//Log.i(TAG, "🚀 GNSS начал работу");
if (callback != null) {
callback.onGPSStatusChanged(1); // GPS_EVENT_STARTED
}
// При старте GNSS сразу запрашиваем обновления
requestLocationUpdates();
}
@Override
public void onStopped() {
//Log.i(TAG, "⏹️ GNSS остановлен");
if (callback != null) {
callback.onGPSStatusChanged(2); // GPS_EVENT_STOPPED
}
}
@Override
public void onFirstFix(int ttffMillis) {
//Log.i(TAG, "🎯 GNSS получил первое фиксирование за " + ttffMillis + "мс");
if (callback != null) {
callback.onGPSStatusChanged(3); // GPS_EVENT_FIRST_FIX
}
// При первом фиксировании также запрашиваем обновления
requestLocationUpdates();
}
@Override
public void onSatelliteStatusChanged(GnssStatus status) {
int count = status.getSatelliteCount();
//Log.d(TAG, "GNSS статус спутников изменился, количество: " + count);
// Подсчитываем количество спутников
satelliteCount = count;
// Логируем детали по спутникам
for (int i = 0; i < count; i++) {
int constellationType = status.getConstellationType(i);
float cn0DbHz = status.getCn0DbHz(i);
boolean usedInFix = status.usedInFix(i);
String constellationName = getConstellationName(constellationType);
//Log.d(TAG, " Спутник " + i + ": " + constellationName +
// ", сигнал=" + cn0DbHz + "dB-Hz, используется=" + usedInFix);
}
if (callback != null) {
callback.onGPSStatusChanged(4); // GPS_EVENT_SATELLITE_STATUS
}
// Если есть спутники, но нет NMEA - принудительно активируем GPS
if (count > 0 && !isListening) {
//Log.i(TAG, "Спутники видны, но слушатель не активен. Активируем GPS...");
// requestLocationUpdates();
}
}
};
locationManager.registerGnssStatusCallback(gnssStatusCallback, new Handler(Looper.getMainLooper()));
//Log.i(TAG, "✅ GNSS статус callback зарегистрирован (новый API)");
}
// Для старых версий Android сразу запрашиваем обновления
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
//Log.i(TAG, "📱 Старая версия Android, сразу запрашиваем location updates");
requestLocationUpdates();
}
// ВАЖНО: Принудительно активируем GPS для получения NMEA
//Log.i(TAG, "🔧 Принудительно активируем GPS для получения NMEA...");
requestLocationUpdates();
// Запускаем таймер для принудительной повторной активации
startActivationTimer();
isListening = true;
//Log.i(TAG, "🎉 NMEA слушатель успешно запущен!");
//Log.i(TAG, "💡 Теперь GPS должен начать отправлять NMEA сообщения");
return true;
} catch (SecurityException e) {
//Log.e(TAG, "❌ Нет разрешений для доступа к GPS: " + e.getMessage());
if (callback != null) {
callback.onError("Нет разрешений для доступа к GPS");
}
return false;
} catch (Exception e) {
//Log.e(TAG, "❌ Ошибка при запуске NMEA слушателя: " + e.getMessage());
e.printStackTrace();
if (callback != null) {
callback.onError("Ошибка запуска: " + e.getMessage());
}
return false;
}
}
/**
* Запрашивает обновления локации для активации GPS
*/
private void requestLocationUpdates() {
try {
//Log.i(TAG, "🔧 Запрашиваем обновления локации для активации NMEA...");
// Создаем слушатель локации с минимальными интервалами
locationListener = new LocationListener() {
@Override
public void onLocationChanged(android.location.Location location) {
//Log.d(TAG, "📍 Location обновлен: " + location.getLatitude() + ", " + location.getLongitude());
//Log.d(TAG, "📍 Точность: " + location.getAccuracy() + "м, время: " + location.getTime());
}
@Override
public void onStatusChanged(String provider, int status, android.os.Bundle extras) {
String statusName = getLocationStatusName(status);
//Log.d(TAG, "📍 Location статус изменился: " + provider + " = " + status + " (" + statusName + ")");
}
@Override
public void onProviderEnabled(String provider) {
//Log.i(TAG, "📍 Location провайдер включен: " + provider);
}
@Override
public void onProviderDisabled(String provider) {
//Log.w(TAG, "📍 Location провайдер отключен: " + provider);
}
};
// Запрашиваем обновления с минимальными интервалами для активации GPS
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
100L, // минимальный интервал в мс (long вместо int)
0.0f, // минимальное расстояние в метрах (float вместо int)
locationListener,
Looper.getMainLooper() // Looper вместо Handler
);
//Log.i(TAG, "✅ Location updates запрошены с минимальным интервалом (100мс)");
// Дополнительно запрашиваем одиночное обновление для принудительной активации
try {
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER,
new LocationListener() {
@Override public void onLocationChanged(android.location.Location location) {
//Log.i(TAG, "🎯 Одиночное обновление получено: " + location.getLatitude() + ", " + location.getLongitude());
}
@Override public void onStatusChanged(String provider, int status, android.os.Bundle extras) {}
@Override public void onProviderEnabled(String provider) {}
@Override public void onProviderDisabled(String provider) {}
}, Looper.getMainLooper()); // Looper вместо Handler
//Log.i(TAG, "✅ Одиночное обновление запрошено");
} catch (Exception e) {
//Log.w(TAG, "⚠️ Не удалось запросить одиночное обновление: " + e.getMessage());
}
// Дополнительно запрашиваем обновления от всех доступных провайдеров
try {
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
1000L, // 1 секунда
0.0f,
locationListener,
Looper.getMainLooper()
);
//Log.i(TAG, "✅ Network провайдер также активирован");
}
} catch (Exception e) {
//Log.w(TAG, "⚠️ Не удалось активировать network провайдер: " + e.getMessage());
}
} catch (Exception e) {
//Log.e(TAG, "❌ Ошибка при запросе location updates: " + e.getMessage());
}
}
/**
* Останавливает прослушивание NMEA сообщений
*/
public void stopListening() {
if (!isListening) {
return;
}
try {
if (locationManager != null) {
// Убираем NMEA слушатель
locationManager.removeNmeaListener(this);
// Убираем GNSS статус callback
if (gnssStatusCallback != null) {
locationManager.unregisterGnssStatusCallback(gnssStatusCallback);
gnssStatusCallback = null;
}
// Убираем GPS статус слушатель (старый API)
if (gpsStatusListener != null) {
locationManager.removeGpsStatusListener(gpsStatusListener);
gpsStatusListener = null;
}
// Убираем Location updates
if (locationListener != null) {
locationManager.removeUpdates(locationListener);
locationListener = null;
//Log.i(TAG, "Location updates остановлены");
}
}
// Останавливаем таймер активации
stopActivationTimer();
isListening = false;
hasReceivedNMEA = false;
//Log.i(TAG, "NMEA слушатель остановлен");
} catch (Exception e) {
//Log.e(TAG, "Ошибка при остановке NMEA слушателя: " + e.getMessage());
}
}
/**
* Callback для NMEA сообщений (API 24+)
*/
@Override
public void onNmeaMessage(String message, long timestamp) {
if (message == null || message.trim().isEmpty()) {
//Log.w(TAG, "⚠️ Получено пустое NMEA сообщение");
return;
}
// Отмечаем, что NMEA получено
hasReceivedNMEA = true;
// Анализируем тип NMEA сообщения
String messageType = getNMEAMessageType(message);
String LogPrefix = "🎯 NMEA [" + messageType + "]";
//Log.i(TAG, //LogPrefix + " получено: " + message);
//Log.d(TAG, //LogPrefix + " timestamp: " + timestamp + " (" + new java.util.Date(timestamp) + ")");
// Дополнительная диагностика для RMC сообщений
if ("RMC".equals(messageType)) {
//Log.i(TAG, //LogPrefix + " 🚢 RMC сообщение получено - GPS активен!");
// Можно добавить парсинг RMC для получения дополнительной информации
parseRMC(message);
}
// Останавливаем таймер активации, так как NMEA приходит
stopActivationTimer();
if (callback != null) {
//Log.d(TAG, //LogPrefix + " Отправляем сообщение в callback");
callback.onNMEAMessage(message, timestamp);
} else {
//Log.w(TAG, //LogPrefix + " ❌ Callback не установлен!");
}
}
/**
* Определяет тип NMEA сообщения
*/
private String getNMEAMessageType(String message) {
if (message == null || message.length() < 6) {
return "UNKNOWN";
}
// NMEA сообщения начинаются с $ и содержат тип после $
if (message.startsWith("$")) {
String[] parts = message.split(",");
if (parts.length > 0) {
// Убираем $ и берем первые 3 символа
String type = parts[0].substring(1);
if (type.length() >= 3) {
return type.substring(0, 3);
}
}
}
return "UNKNOWN";
}
/**
* Парсит RMC сообщение для диагностики
*/
private void parseRMC(String message) {
try {
String[] parts = message.split(",");
if (parts.length >= 12) {
String time = parts[1];
String status = parts[2];
String lat = parts[3];
String latDir = parts[4];
String lon = parts[5];
String lonDir = parts[6];
String speed = parts[7];
String course = parts[8];
String date = parts[9];
//Log.d(TAG, "🚢 RMC детали:");
//Log.d(TAG, " Время: " + time + ", Дата: " + date);
//Log.d(TAG, " Статус: " + (status.equals("A") ? "Активен" : "Неактивен"));
//Log.d(TAG, " Координаты: " + lat + latDir + ", " + lon + lonDir);
//Log.d(TAG, " Скорость: " + speed + " узлов, Курс: " + course + "°");
}
} catch (Exception e) {
//Log.w(TAG, "⚠️ Не удалось распарсить RMC: " + e.getMessage());
}
}
/**
* Проверяет, запущен ли слушатель
*/
public boolean isListening() {
return isListening;
}
/**
* Проверяет доступность GPS
*/
public boolean isGPSAvailable() {
if (locationManager == null) {
return false;
}
boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
boolean networkEnabled = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
boolean passiveEnabled = locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER);
//Log.d(TAG, "GPS провайдер: " + (gpsEnabled ? "включен" : "отключен"));
//Log.d(TAG, "Network провайдер: " + (networkEnabled ? "включен" : "отключен"));
//Log.d(TAG, "Passive провайдер: " + (passiveEnabled ? "включен" : "отключен"));
return gpsEnabled;
}
/**
* Получает детальную информацию о состоянии GPS
*/
public String getGPSStatusInfo() {
if (locationManager == null) {
return "LocationManager недоступен";
}
StringBuilder info = new StringBuilder();
info.append("GPS провайдер: ").append(locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) ? "включен" : "отключен").append("\n");
info.append("Network провайдер: ").append(locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) ? "включен" : "отключен").append("\n");
info.append("Passive провайдер: ").append(locationManager.isProviderEnabled(LocationManager.PASSIVE_PROVIDER) ? "включен" : "отключен").append("\n");
info.append("Спутников: ").append(satelliteCount).append("\n");
info.append("Слушатель активен: ").append(isListening ? "да" : "нет");
return info.toString();
}
/**
* Принудительно активирует GPS для получения NMEA
*/
public void forceGPSActivation() {
if (!isListening) {
//Log.w(TAG, "Слушатель не запущен, запускаем...");
startListening();
return;
}
//Log.i(TAG, "Принудительно активируем GPS...");
// Запрашиваем обновления локации с минимальными интервалами
try {
if (locationListener != null) {
locationManager.removeUpdates(locationListener);
}
requestLocationUpdates();
// Дополнительно запрашиваем одиночное обновление
locationManager.requestSingleUpdate(LocationManager.GPS_PROVIDER,
new LocationListener() {
@Override public void onLocationChanged(android.location.Location location) {
//Log.i(TAG, "Одиночное обновление получено: " + location.getLatitude() + ", " + location.getLongitude());
}
@Override public void onStatusChanged(String provider, int status, android.os.Bundle extras) {}
@Override public void onProviderEnabled(String provider) {}
@Override public void onProviderDisabled(String provider) {}
}, Looper.getMainLooper()); // Looper вместо Handler
//Log.i(TAG, "Принудительная активация GPS выполнена");
} catch (Exception e) {
//Log.e(TAG, "Ошибка при принудительной активации GPS: " + e.getMessage());
}
}
/**
* Запускает таймер для принудительной повторной активации GPS
*/
private void startActivationTimer() {
// Останавливаем предыдущий таймер
activationHandler.removeCallbacksAndMessages(null);
// Запускаем таймер на 5 секунд
activationHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (isListening && !hasReceivedNMEA) {
//Log.w(TAG, "⏰ Таймер активации: NMEA не получено, принудительно активируем GPS...");
forceGPSActivation();
// Повторяем каждые 10 секунд, пока не получим NMEA
activationHandler.postDelayed(this, 10000);
}
}
}, 5000);
//Log.i(TAG, "⏰ Таймер активации GPS запущен (5 сек)");
}
/**
* Останавливает таймер активации
*/
private void stopActivationTimer() {
activationHandler.removeCallbacksAndMessages(null);
//Log.d(TAG, "⏰ Таймер активации GPS остановлен");
}
/**
* Получает количество спутников
*/
public int getSatelliteCount() {
return satelliteCount;
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
if (isListening) {
stopListening();
}
// Останавливаем таймер активации
stopActivationTimer();
locationManager = null;
activationHandler = null;
}
/**
* Получает название GPS события
*/
private String getGpsEventName(int event) {
switch (event) {
case GpsStatus.GPS_EVENT_STARTED: return "GPS_EVENT_STARTED";
case GpsStatus.GPS_EVENT_STOPPED: return "GPS_EVENT_STOPPED";
case GpsStatus.GPS_EVENT_FIRST_FIX: return "GPS_EVENT_FIRST_FIX";
case GpsStatus.GPS_EVENT_SATELLITE_STATUS: return "GPS_EVENT_SATELLITE_STATUS";
default: return "UNKNOWN(" + event + ")";
}
}
/**
* Получает название созвездия спутников
*/
private String getConstellationName(int constellationType) {
switch (constellationType) {
case GnssStatus.CONSTELLATION_GPS: return "GPS";
case GnssStatus.CONSTELLATION_GLONASS: return "GLONASS";
case GnssStatus.CONSTELLATION_BEIDOU: return "BeiDou";
case GnssStatus.CONSTELLATION_GALILEO: return "Galileo";
case GnssStatus.CONSTELLATION_QZSS: return "QZSS";
case GnssStatus.CONSTELLATION_SBAS: return "SBAS";
case GnssStatus.CONSTELLATION_IRNSS: return "IRNSS";
default: return "Unknown(" + constellationType + ")";
}
}
/**
* Получает название статуса локации
*/
private String getLocationStatusName(int status) {
switch (status) {
case android.location.LocationProvider.AVAILABLE: return "AVAILABLE";
case android.location.LocationProvider.TEMPORARILY_UNAVAILABLE: return "TEMPORARILY_UNAVAILABLE";
case android.location.LocationProvider.OUT_OF_SERVICE: return "OUT_OF_SERVICE";
default: return "UNKNOWN(" + status + ")";
}
}
}
@@ -0,0 +1,595 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.maps.MapInterface;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* Главный контроллер приложения
* Координирует работу всех компонентов
* Использует гибридный подход: координаты через Location API, остальное через NMEA
*/
public class AppController implements
NMEAParser.NMEAParserListener,
UDPListener.UDPListenerCallback,
AndroidNMEAListener.NMEAMessageCallback,
GPSLocationListener.LocationCallback,
MapInterface.MarkerClickListener {
private static final String TAG = "AppController";
private Context context;
private NMEAParser nmeaParser;
private UDPListener udpListener;
private AndroidNMEAListener androidNmeaListener;
private GPSLocationListener gpsLocationListener;
private MapInterface mapInterface;
private Vessel ownVessel;
private List<AISVessel> aisVessels;
private ExecutorService executor;
private boolean isUDPEnabled;
private boolean isAndroidNMEAEnabled;
private boolean isGPSLocationEnabled;
// Callback для обновления UI
private UIUpdateCallback uiUpdateCallback;
public interface UIUpdateCallback {
void onVesselPositionUpdated(Vessel vessel);
void onGPSQualityUpdated(Vessel vessel);
}
/**
* Расширенный интерфейс для дополнительных UI событий
*/
public interface ExtendedUIUpdateCallback extends UIUpdateCallback {
void onShowOwnVesselBottomSheet();
void onShowAISVesselInfo(AISVessel vessel);
void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels);
}
public AppController(Context context) {
this.context = context;
this.ownVessel = new Vessel();
this.aisVessels = new ArrayList<>();
this.executor = Executors.newCachedThreadPool();
initializeControllers();
}
/**
* Инициализирует все контроллеры
*/
private void initializeControllers() {
// Инициализация парсера NMEA
nmeaParser = new NMEAParser();
nmeaParser.setListener(this);
// Инициализация GPS Location Listener (для координат)
gpsLocationListener = new GPSLocationListener(context);
gpsLocationListener.setCallback(this);
// Связываем NMEA парсер с GPS Location Listener для гибридного режима
nmeaParser.setGPSLocationListener(gpsLocationListener);
nmeaParser.setHybridMode(true);
// Инициализация UDP слушателя (порт 10110 - стандартный для AIS)
udpListener = new UDPListener(10110);
udpListener.setCallback(this);
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
androidNmeaListener = new AndroidNMEAListener(context);
androidNmeaListener.setCallback(this);
}
/**
* Устанавливает интерфейс карты
*/
public void setMapInterface(MapInterface mapInterface) {
Log.i(TAG, "setMapInterface вызван: " + (mapInterface != null ? "mapInterface установлен" : "mapInterface == null"));
this.mapInterface = mapInterface;
if (mapInterface != null) {
Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface");
mapInterface.setMarkerClickListener(this);
Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры");
}
}
/**
* Устанавливает callback для обновления UI
*/
public void setUIUpdateCallback(UIUpdateCallback callback) {
this.uiUpdateCallback = callback;
}
/**
* Запускает все слушатели
*/
public void startAllListeners() {
// GPS Location Listener запускается в главном потоке
if (isGPSLocationEnabled) {
gpsLocationListener.startListening();
}
// Android NMEA слушатель должен запускаться в главном потоке
if (isAndroidNMEAEnabled) {
androidNmeaListener.startListening();
}
// UDP слушатель запускается в фоновом потоке
if (isUDPEnabled) {
executor.execute(() -> {
udpListener.start();
});
}
// Тестируем NMEA парсер (временно)
testNMEAParser();
}
/**
* Тестирует NMEA парсер (временно для отладки)
*/
private void testNMEAParser() {
Log.i(TAG, "Тестируем NMEA парсер...");
// Тестовые NMEA сообщения
String[] testMessages = {
"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A",
"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48",
"$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.8,1.0*3E"
};
for (String message : testMessages) {
Log.i(TAG, "Тестируем сообщение: " + message);
nmeaParser.parseNMEA(message);
}
}
/**
* Останавливает все слушатели
*/
public void stopAllListeners() {
executor.execute(() -> {
udpListener.stop();
androidNmeaListener.stopListening();
gpsLocationListener.stopListening();
});
}
/**
* Включает/выключает UDP слушатель
*/
public void setUDPEnabled(boolean enabled) {
this.isUDPEnabled = enabled;
if (enabled && !udpListener.isRunning()) {
udpListener.start();
} else if (!enabled && udpListener.isRunning()) {
udpListener.stop();
}
}
/**
* Включает/выключает Android NMEA слушатель
*/
public void setAndroidNMEAEnabled(boolean enabled) {
Log.i(TAG, "🔄 setAndroidNMEAEnabled: " + enabled);
this.isAndroidNMEAEnabled = enabled;
// Android NMEA слушатель управляется в главном потоке
if (enabled && !androidNmeaListener.isListening()) {
Log.i(TAG, "🚀 Запускаем Android NMEA слушатель...");
boolean success = androidNmeaListener.startListening();
if (success) {
Log.i(TAG, "✅ Android NMEA слушатель успешно запущен");
} else {
Log.e(TAG, "❌ Не удалось запустить Android NMEA слушатель");
}
} else if (!enabled && androidNmeaListener.isListening()) {
Log.i(TAG, "⏹️ Останавливаем Android NMEA слушатель...");
androidNmeaListener.stopListening();
}
}
/**
* Включает/выключает GPS Location слушатель
*/
public void setGPSLocationEnabled(boolean enabled) {
Log.i(TAG, "🔄 setGPSLocationEnabled: " + enabled);
this.isGPSLocationEnabled = enabled;
if (enabled && !gpsLocationListener.isListening()) {
Log.i(TAG, "🚀 Запускаем GPS Location слушатель...");
boolean success = gpsLocationListener.startListening();
if (success) {
Log.i(TAG, "✅ GPS Location слушатель успешно запущен");
} else {
Log.e(TAG, "❌ Не удалось запустить GPS Location слушатель");
}
} else if (!enabled && gpsLocationListener.isListening()) {
Log.i(TAG, "⏹️ Останавливаем GPS Location слушатель...");
gpsLocationListener.stopListening();
}
}
/**
* Устанавливает UDP порт
*/
public void setUDPPort(int port) {
udpListener.setPort(port);
}
/**
* Отправляет данные по UDP
*/
public void sendUDPData(String data, String address, int port) {
udpListener.sendData(data, address, port);
}
/**
* Проверяет, включен ли UDP слушатель
*/
public boolean isUDPEnabled() {
return isUDPEnabled;
}
/**
* Проверяет, включен ли Android NMEA слушатель
*/
public boolean isAndroidNMEAEnabled() {
return isAndroidNMEAEnabled;
}
/**
* Проверяет, включен ли GPS Location слушатель
*/
public boolean isGPSLocationEnabled() {
return isGPSLocationEnabled;
}
/**
* Обновляет данные нашего судна при клике по маркеру
*/
private void updateOwnVesselData(Vessel vessel) {
if (vessel != null) {
// Обновляем только те данные, которые могут быть актуальными
// Координаты и основная информация уже обновляются через GPS
if (vessel.getCourse() > 0) {
ownVessel.setCourse(vessel.getCourse());
updateCompass(); // Обновляем компас при изменении курса
}
if (vessel.getSpeed() > 0) {
ownVessel.setSpeed(vessel.getSpeed());
}
if (vessel.getSatellites() > 0) {
ownVessel.setSatellites(vessel.getSatellites());
}
if (vessel.getAltitude() != 0) {
ownVessel.setAltitude(vessel.getAltitude());
}
if (vessel.getPdop() > 0) {
ownVessel.setPdop(vessel.getPdop());
ownVessel.setHdop(vessel.getHdop());
ownVessel.setVdop(vessel.getVdop());
}
}
}
// Реализация LocationCallback (GPS Location Listener)
@Override
public void onLocationUpdated(Vessel vessel) {
Log.i(TAG, "📍 GPS Location обновлен: lat=" + vessel.getLatitude() +
", lon=" + vessel.getLongitude() +
", accuracy=" + vessel.getAccuracy() + "м");
// Обновляем координаты нашего судна
ownVessel.setLatitude(vessel.getLatitude());
ownVessel.setLongitude(vessel.getLongitude());
ownVessel.setAccuracy(vessel.getAccuracy());
ownVessel.setFixTime(vessel.getFixTime());
ownVessel.setFixQuality(vessel.getFixQuality());
// Обновляем UI через callback
if (uiUpdateCallback != null) {
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
}
// Обновляем карту в главном потоке
if (mapInterface != null) {
Log.i(TAG, "Обновляем позицию на карте...");
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
try {
Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition...");
mapInterface.updateOwnVesselPosition(ownVessel);
Log.i(TAG, "Позиция на карте обновлена");
} catch (Exception e) {
Log.e(TAG, "Ошибка обновления позиции на карте: " + e.getMessage(), e);
}
});
}
}
@Override
public void onGPSStatusChanged(int status) {
Log.i(TAG, "GPS статус изменился: " + status);
}
// Реализация NMEAParserListener
@Override
public void onVesselUpdated(Vessel vessel) {
// В гибридном режиме обновляем только дополнительные данные
if (vessel.getCourse() > 0) {
ownVessel.setCourse(vessel.getCourse());
updateCompass(); // Обновляем компас при изменении курса
}
if (vessel.getSpeed() > 0) {
ownVessel.setSpeed(vessel.getSpeed());
}
if (vessel.getSatellites() > 0) {
ownVessel.setSatellites(vessel.getSatellites());
}
if (vessel.getAltitude() != 0) {
ownVessel.setAltitude(vessel.getAltitude());
}
Log.i(TAG, "NMEA данные обновлены: course=" + vessel.getCourse() +
", speed=" + vessel.getSpeed() +
", satellites=" + vessel.getSatellites());
// Обновляем UI
if (uiUpdateCallback != null) {
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
}
}
@Override
public void onDOPUpdated(double pdop, double hdop, double vdop) {
Log.i(TAG, "📊 DOP обновлен: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
// Обновляем DOP значения
ownVessel.setPdop(pdop);
ownVessel.setHdop(hdop);
ownVessel.setVdop(vdop);
// Обновляем UI
if (uiUpdateCallback != null) {
uiUpdateCallback.onGPSQualityUpdated(ownVessel);
}
}
@Override
public void onAISVesselUpdated(AISVessel vessel) {
// Проверяем, есть ли уже такое судно
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
if (existingVessel != null) {
// Обновляем существующее судно
existingVessel.updatePosition(
vessel.getLatitude(),
vessel.getLongitude(),
vessel.getCourse(),
vessel.getSpeed()
);
if (mapInterface != null) {
// Используем Handler для выполнения в главном потоке
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
try {
mapInterface.updateAISVesselPosition(existingVessel);
} catch (Exception e) {
Log.e(TAG, "Ошибка обновления позиции AIS судна на карте: " + e.getMessage(), e);
}
});
}
} else {
// Добавляем новое судно
aisVessels.add(vessel);
if (mapInterface != null) {
// Используем Handler для выполнения в главном потоке
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
try {
mapInterface.addAISVesselMarker(vessel);
} catch (Exception e) {
Log.e(TAG, "Ошибка добавления AIS судна на карту: " + e.getMessage(), e);
}
});
}
}
// Обновляем компас с ближайшими судами
updateCompass();
Log.i(TAG, "AIS судно обновлено: " + vessel);
}
@Override
public void onParseError(String error) {
Log.e(TAG, "Ошибка парсинга NMEA: " + error);
}
/**
* Обновляет компас с текущим азимутом и ближайшими судами
*/
private void updateCompass() {
if (uiUpdateCallback instanceof ExtendedUIUpdateCallback) {
float azimuth = (float) ownVessel.getCourse();
List<AISVessel> nearbyVessels = getNearbyVessels();
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
((ExtendedUIUpdateCallback) uiUpdateCallback).onUpdateCompass(azimuth, nearbyVessels);
});
}
}
/**
* Получает список ближайших судов (в пределах 10 км)
*/
private List<AISVessel> getNearbyVessels() {
List<AISVessel> nearby = new ArrayList<>();
double maxDistance = 10000; // 10 км в метрах
for (AISVessel vessel : aisVessels) {
double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel);
if (distance <= maxDistance) {
nearby.add(vessel);
}
}
return nearby;
}
// Реализация UDPListenerCallback
@Override
public void onDataReceived(String data, String sourceAddress, int sourcePort) {
Log.d(TAG, "UDP данные получены от " + sourceAddress + ":" + sourcePort);
// Парсим полученные данные как NMEA
nmeaParser.parseNMEA(data);
}
@Override
public void onUDPError(String error) {
Log.e(TAG, "UDP ошибка: " + error);
}
@Override
public void onError(String error) {
Log.e(TAG, "GPS Location ошибка: " + error);
}
// Реализация NMEAMessageCallback
@Override
public void onNMEAMessage(String message, long timestamp) {
Log.i(TAG, "📱 Android NMEA сообщение получено в AppController: " + message);
// Парсим полученные данные как NMEA
nmeaParser.parseNMEA(message);
}
// Реализация MarkerClickListener
@Override
public void onOwnVesselClick(Vessel vessel) {
Log.i(TAG, "Клик по нашему судну: " + vessel);
// Уведомляем UI о необходимости показать BottomSheet
if (uiUpdateCallback != null) {
Log.i(TAG, "uiUpdateCallback найден, обновляем данные судна");
// Обновляем данные судна перед показом
updateOwnVesselData(vessel);
// Вызываем специальный callback для показа BottomSheet
if (uiUpdateCallback instanceof ExtendedUIUpdateCallback) {
Log.i(TAG, "Вызываем onShowOwnVesselBottomSheet");
((ExtendedUIUpdateCallback) uiUpdateCallback).onShowOwnVesselBottomSheet();
} else {
Log.w(TAG, "uiUpdateCallback не является ExtendedUIUpdateCallback");
}
} else {
Log.e(TAG, "uiUpdateCallback == null!");
}
}
@Override
public void onAISVesselClick(AISVessel vessel) {
Log.i(TAG, "Клик по AIS судну: " + vessel);
// Уведомляем UI о необходимости показать информацию об AIS судне
if (uiUpdateCallback != null && uiUpdateCallback instanceof ExtendedUIUpdateCallback) {
((ExtendedUIUpdateCallback) uiUpdateCallback).onShowAISVesselInfo(vessel);
}
}
/**
* Находит AIS судно по MMSI
*/
private AISVessel findAISVesselByMMSI(String mmsi) {
for (AISVessel vessel : aisVessels) {
if (mmsi.equals(vessel.getMmsi())) {
return vessel;
}
}
return null;
}
/**
* Получает наше судно
*/
public Vessel getOwnVessel() {
return ownVessel;
}
/**
* Получает список AIS судов
*/
public List<AISVessel> getAISVessels() {
return new ArrayList<>(aisVessels);
}
/**
* Очищает все AIS суда
*/
public void clearAISVessels() {
aisVessels.clear();
if (mapInterface != null) {
// Используем Handler для выполнения в главном потоке
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
try {
mapInterface.clearAISVesselMarkers();
} catch (Exception e) {
Log.e(TAG, "Ошибка очистки AIS судов на карте: " + e.getMessage(), e);
}
});
}
}
/**
* Центрирует карту на позиции нашего судна
*/
public void centerOnOwnVessel() {
if (mapInterface != null && ownVessel != null) {
// Используем Handler для выполнения в главном потоке
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
try {
mapInterface.centerOnPosition(ownVessel.getLatitude(), ownVessel.getLongitude());
} catch (Exception e) {
Log.e(TAG, "Ошибка центрирования карты: " + e.getMessage(), e);
}
});
}
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stopAllListeners();
if (udpListener != null) {
udpListener.cleanup();
}
if (androidNmeaListener != null) {
androidNmeaListener.cleanup();
}
if (gpsLocationListener != null) {
gpsLocationListener.cleanup();
}
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
}
@@ -0,0 +1,184 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
import com.grigowashere.aismap.models.Vessel;
/**
* Тестовый класс для демонстрации работы гибридного GPS подхода
* Показывает, как координаты получаются через Location API, а остальное через NMEA
*/
public class GPSHybridTest {
private static final String TAG = "GPSHybridTest";
private Context context;
private GPSLocationListener gpsLocationListener;
private NMEAParser nmeaParser;
private Vessel testVessel;
public GPSHybridTest(Context context) {
this.context = context;
this.testVessel = new Vessel();
// Инициализируем компоненты
gpsLocationListener = new GPSLocationListener(context);
nmeaParser = new NMEAParser();
// Связываем их для гибридного режима
nmeaParser.setGPSLocationListener(gpsLocationListener);
nmeaParser.setHybridMode(true);
// Устанавливаем callback'и
gpsLocationListener.setCallback(new GPSLocationListener.LocationCallback() {
@Override
public void onLocationUpdated(Vessel vessel) {
Log.i(TAG, "📍 GPS Location получен: " + vessel.getLatitude() + ", " + vessel.getLongitude());
Log.i(TAG, "📍 Точность: " + vessel.getAccuracy() + "м");
// Обновляем координаты
testVessel.setLatitude(vessel.getLatitude());
testVessel.setLongitude(vessel.getLongitude());
testVessel.setAccuracy(vessel.getAccuracy());
testVessel.setFixTime(vessel.getFixTime());
testVessel.setFixQuality(vessel.getFixQuality());
logVesselStatus();
}
@Override
public void onGPSStatusChanged(int status) {
Log.i(TAG, "GPS статус: " + status);
}
@Override
public void onError(String error) {
Log.e(TAG, "GPS Location ошибка: " + error);
}
});
nmeaParser.setListener(new NMEAParser.NMEAParserListener() {
@Override
public void onVesselUpdated(Vessel vessel) {
Log.i(TAG, "📡 NMEA данные получены: course=" + vessel.getCourse() +
", speed=" + vessel.getSpeed() +
", satellites=" + vessel.getSatellites());
// Обновляем дополнительные данные
if (vessel.getCourse() > 0) testVessel.setCourse(vessel.getCourse());
if (vessel.getSpeed() > 0) testVessel.setSpeed(vessel.getSpeed());
if (vessel.getSatellites() > 0) testVessel.setSatellites(vessel.getSatellites());
if (vessel.getAltitude() != 0) testVessel.setAltitude(vessel.getAltitude());
logVesselStatus();
}
@Override
public void onAISVesselUpdated(com.grigowashere.aismap.models.AISVessel vessel) {
Log.i(TAG, "🚢 AIS судно: " + vessel);
}
@Override
public void onParseError(String error) {
Log.e(TAG, "NMEA ошибка: " + error);
}
@Override
public void onDOPUpdated(double pdop, double hdop, double vdop) {
Log.i(TAG, "📊 DOP: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
testVessel.setPdop(pdop);
testVessel.setHdop(hdop);
testVessel.setVdop(vdop);
logVesselStatus();
}
});
}
/**
* Запускает тест
*/
public void startTest() {
Log.i(TAG, "🚀 Запускаем тест гибридного GPS подхода...");
// Запускаем GPS Location Listener
boolean gpsSuccess = gpsLocationListener.startListening();
if (gpsSuccess) {
Log.i(TAG, "✅ GPS Location Listener запущен");
} else {
Log.e(TAG, "❌ Не удалось запустить GPS Location Listener");
}
// Тестируем NMEA парсер с тестовыми сообщениями
testNMEAParser();
}
/**
* Тестирует NMEA парсер
*/
private void testNMEAParser() {
Log.i(TAG, "🧪 Тестируем NMEA парсер...");
// Тестовые NMEA сообщения
String[] testMessages = {
// GGA - количество спутников и высота
"$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47",
// RMC - курс и скорость
"$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A",
// VTG - курс и скорость (альтернативный источник)
"$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48",
// GSA - DOP и активные спутники
"$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.8,1.0*3E",
// GSV - спутники в поле зрения
"$GPGSV,3,1,12,01,05,040,3,02,46,000,4,03,42,350,4,04,42,000,4*7F"
};
for (String message : testMessages) {
Log.i(TAG, "📡 Тестируем NMEA: " + message);
nmeaParser.parseNMEA(message);
}
}
/**
* Логирует текущий статус судна
*/
private void logVesselStatus() {
Log.i(TAG, "🚢 === СТАТУС СУДНА ===");
Log.i(TAG, "📍 Координаты: " + testVessel.getLatitude() + ", " + testVessel.getLongitude());
Log.i(TAG, "🎯 Точность: " + testVessel.getAccuracy() + "м (" + testVessel.getGPSQualityDescription() + ")");
Log.i(TAG, "🧭 Курс: " + testVessel.getCourse() + "°");
Log.i(TAG, "⚡ Скорость: " + testVessel.getSpeed() + " узлов");
Log.i(TAG, "Спутники: " + testVessel.getSatellites() + "/" + testVessel.getActiveSatellites());
Log.i(TAG, "📊 DOP: PDOP=" + testVessel.getPdop() + ", HDOP=" + testVessel.getHdop() + ", VDOP=" + testVessel.getVdop());
Log.i(TAG, "🏔️ Высота: " + testVessel.getAltitude() + "м");
Log.i(TAG, "🔧 Качество фикса: " + testVessel.getFixQuality());
Log.i(TAG, "==========================");
}
/**
* Останавливает тест
*/
public void stopTest() {
Log.i(TAG, "⏹️ Останавливаем тест...");
if (gpsLocationListener != null) {
gpsLocationListener.stopListening();
}
Log.i(TAG, "✅ Тест остановлен");
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
if (gpsLocationListener != null) {
gpsLocationListener.cleanup();
}
}
}
@@ -0,0 +1,350 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Build;
import android.os.Bundle;
import android.os.Looper;
import android.util.Log;
import android.location.GnssStatus;
import android.location.GpsStatus;
import android.location.GpsSatellite;
import com.grigowashere.aismap.models.Vessel;
/**
* Слушатель GPS координат через стандартный Android Location API
* Более надежен чем NMEA для получения позиции
*/
public class GPSLocationListener implements LocationListener {
private static final String TAG = "GPSLocationListener";
private Context context;
private LocationManager locationManager;
private LocationCallback callback;
private boolean isListening;
// GPS статус
private int satelliteCount;
private int activeSatellites;
private double pdop = -1.0;
private double hdop = -1.0;
private double vdop = -1.0;
// Callback интерфейс
public interface LocationCallback {
void onLocationUpdated(Vessel vessel);
void onGPSStatusChanged(int status);
void onError(String error);
}
public GPSLocationListener(Context context) {
this.context = context;
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
this.isListening = false;
}
public void setCallback(LocationCallback callback) {
this.callback = callback;
}
/**
* Запускает прослушивание GPS координат
*/
public boolean startListening() {
if (isListening) {
Log.w(TAG, "GPS слушатель уже запущен");
return true;
}
if (locationManager == null) {
Log.e(TAG, "LocationManager недоступен");
if (callback != null) {
callback.onError("LocationManager недоступен");
}
return false;
}
// Проверяем GPS провайдер
if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
Log.w(TAG, "GPS провайдер отключен");
if (callback != null) {
callback.onError("GPS провайдер отключен");
}
return false;
}
// Проверяем разрешения
if (context.checkSelfPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
!= android.content.pm.PackageManager.PERMISSION_GRANTED) {
Log.e(TAG, "Нет разрешения ACCESS_FINE_LOCATION");
if (callback != null) {
callback.onError("Нет разрешения ACCESS_FINE_LOCATION");
}
return false;
}
try {
Log.i(TAG, "=== ЗАПУСК GPS LOCATION LISTENER ===");
// Регистрируем слушатель локации с минимальным интервалом
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
100L, // минимальный интервал в мс
0.0f, // минимальное расстояние в метрах
this,
Looper.getMainLooper()
);
Log.i(TAG, "✅ GPS location updates запрошены");
// Регистрируем GNSS статус callback для получения информации о спутниках
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
GnssStatus.Callback gnssCallback = new GnssStatus.Callback() {
@Override
public void onSatelliteStatusChanged(GnssStatus status) {
updateSatelliteInfo(status);
}
};
locationManager.registerGnssStatusCallback(gnssCallback, new android.os.Handler(Looper.getMainLooper()));
Log.i(TAG, "✅ GNSS статус callback зарегистрирован");
}
// Для старых версий Android
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
GpsStatus.Listener gpsListener = new GpsStatus.Listener() {
@Override
public void onGpsStatusChanged(int event) {
if (event == GpsStatus.GPS_EVENT_SATELLITE_STATUS) {
GpsStatus gpsStatus = locationManager.getGpsStatus(null);
if (gpsStatus != null) {
updateSatelliteInfoLegacy(gpsStatus);
}
}
if (callback != null) {
callback.onGPSStatusChanged(event);
}
}
};
locationManager.addGpsStatusListener(gpsListener);
Log.i(TAG, "✅ GPS статус слушатель зарегистрирован (старый API)");
}
isListening = true;
Log.i(TAG, "🎉 GPS Location Listener успешно запущен!");
return true;
} catch (SecurityException e) {
Log.e(TAG, "❌ Нет разрешений для доступа к GPS: " + e.getMessage());
if (callback != null) {
callback.onError("Нет разрешений для доступа к GPS");
}
return false;
} catch (Exception e) {
Log.e(TAG, "❌ Ошибка при запуске GPS слушателя: " + e.getMessage());
e.printStackTrace();
if (callback != null) {
callback.onError("Ошибка запуска: " + e.getMessage());
}
return false;
}
}
/**
* Останавливает прослушивание
*/
public void stopListening() {
if (!isListening) {
return;
}
try {
if (locationManager != null) {
locationManager.removeUpdates(this);
Log.i(TAG, "GPS location updates остановлены");
}
isListening = false;
Log.i(TAG, "GPS Location Listener остановлен");
} catch (Exception e) {
Log.e(TAG, "Ошибка при остановке GPS слушателя: " + e.getMessage());
}
}
/**
* Обновляет информацию о спутниках (новый API)
*/
private void updateSatelliteInfo(GnssStatus status) {
int totalCount = status.getSatelliteCount();
int usedCount = 0;
for (int i = 0; i < totalCount; i++) {
if (status.usedInFix(i)) {
usedCount++;
}
}
satelliteCount = totalCount;
activeSatellites = usedCount;
Log.d(TAG, "Спутники: " + usedCount + "/" + totalCount + " активны");
}
/**
* Обновляет информацию о спутниках (старый API)
*/
private void updateSatelliteInfoLegacy(GpsStatus status) {
int totalCount = 0;
int usedCount = 0;
for (GpsSatellite satellite : status.getSatellites()) {
totalCount++;
if (satellite.usedInFix()) {
usedCount++;
}
}
satelliteCount = totalCount;
activeSatellites = usedCount;
Log.d(TAG, "Спутники (legacy): " + usedCount + "/" + totalCount + " активны");
}
// Реализация LocationListener
@Override
public void onLocationChanged(Location location) {
if (location == null) {
Log.w(TAG, "⚠️ Получен null location");
return;
}
Log.i(TAG, "📍 Location обновлен: " + location.getLatitude() + ", " + location.getLongitude());
Log.i(TAG, "📍 Точность: " + location.getAccuracy() + "м, время: " + location.getTime());
// Создаем объект судна с полученными данными
Vessel vessel = new Vessel();
vessel.setLatitude(location.getLatitude());
vessel.setLongitude(location.getLongitude());
vessel.setAccuracy(location.getAccuracy());
vessel.setFixTime(location.getTime());
// Определяем качество фикса
if (location.hasAccuracy()) {
if (location.getAccuracy() <= 3) {
vessel.setFixQuality("HIGH_ACCURACY");
} else if (location.getAccuracy() <= 10) {
vessel.setFixQuality("GPS");
} else {
vessel.setFixQuality("LOW_ACCURACY");
}
}
// Обновляем информацию о спутниках
vessel.updateGPSQuality(satelliteCount, activeSatellites, pdop, hdop, vdop, location.getAccuracy());
// Отправляем обновление через callback
if (callback != null) {
callback.onLocationUpdated(vessel);
}
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
String statusName = getLocationStatusName(status);
Log.d(TAG, "📍 Location статус изменился: " + provider + " = " + status + " (" + statusName + ")");
if (callback != null) {
callback.onGPSStatusChanged(status);
}
}
@Override
public void onProviderEnabled(String provider) {
Log.i(TAG, "📍 Location провайдер включен: " + provider);
}
@Override
public void onProviderDisabled(String provider) {
Log.w(TAG, "📍 Location провайдер отключен: " + provider);
}
/**
* Устанавливает DOP значения (получаются из NMEA GSA)
*/
public void setDOPValues(double pdop, double hdop, double vdop) {
this.pdop = pdop;
this.hdop = hdop;
this.vdop = vdop;
Log.d(TAG, "📊 DOP обновлен: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
}
/**
* Устанавливает количество спутников в объект Vessel
*/
public void setSatellitesInVessel(Vessel vessel) {
if (vessel != null) {
// НЕ перезаписываем общее количество спутников из NMEA
// vessel.setSatellites(satelliteCount); // Убираем эту строку!
// Устанавливаем только количество активных спутников
vessel.setActiveSatellites(activeSatellites);
Log.d(TAG, "Обновлен Vessel: активных спутников=" + activeSatellites +
" (общее количество из NMEA: " + vessel.getSatellites() + ")");
}
}
/**
* Получает текущее количество спутников
*/
public int getSatelliteCount() {
return satelliteCount;
}
/**
* Получает количество активных спутников
*/
public int getActiveSatellites() {
return activeSatellites;
}
/**
* Получает детальную информацию о спутниках
*/
public String getSatelliteInfo() {
return String.format("Всего: %d, Активных: %d", satelliteCount, activeSatellites);
}
/**
* Проверяет, запущен ли слушатель
*/
public boolean isListening() {
return isListening;
}
/**
* Получает название статуса локации
*/
private String getLocationStatusName(int status) {
switch (status) {
case android.location.LocationProvider.AVAILABLE: return "AVAILABLE";
case android.location.LocationProvider.TEMPORARILY_UNAVAILABLE: return "TEMPORARILY_UNAVAILABLE";
case android.location.LocationProvider.OUT_OF_SERVICE: return "OUT_OF_SERVICE";
default: return "UNKNOWN(" + status + ")";
}
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
if (isListening) {
stopListening();
}
locationManager = null;
}
}
@@ -0,0 +1,136 @@
package com.grigowashere.aismap.controllers;
import android.content.Context;
import android.util.Log;
import com.grigowashere.aismap.maps.MapInterface;
import com.grigowashere.aismap.maps.YandexMapImpl;
import com.yandex.mapkit.mapview.MapView;
/**
* Контроллер для управления картами
* Инкапсулирует логику инициализации и управления различными картами
*/
public class MapController {
private static final String TAG = "MapController";
private static boolean isYandexMapsInitialized = false;
private Context context;
private MapInterface currentMapInterface;
private MapView mapView;
public MapController(Context context) {
this.context = context;
}
/**
* Инициализирует карту указанного типа
*/
public MapInterface initializeMap(String mapType, MapView mapView) {
this.mapView = mapView;
switch (mapType.toLowerCase()) {
case "yandex":
return initializeYandexMaps();
case "mapforge":
return initializeMapForge();
default:
Log.e(TAG, "Неизвестный тип карты: " + mapType);
return null;
}
}
/**
* Инициализирует Яндекс.Карты
*/
private MapInterface initializeYandexMaps() {
try {
// Проверяем, что Яндекс.Карты уже инициализированы
if (!isYandexMapsInitialized) {
Log.w(TAG, "Яндекс.Карты не инициализированы. Должны быть инициализированы в MainActivity");
return null;
}
Log.i(TAG, "Создаем интерфейс для Яндекс.Карт");
// Создаем интерфейс для Яндекс.Карт
currentMapInterface = new YandexMapImpl(context, mapView);
return currentMapInterface;
} catch (Exception e) {
Log.e(TAG, "Ошибка при создании интерфейса Яндекс.Карт: " + e.getMessage());
return null;
}
}
/**
* Инициализирует MapForge
*/
private MapInterface initializeMapForge() {
try {
// TODO: Реализовать инициализацию MapForge
Log.i(TAG, "MapForge инициализация (пока не реализована)");
return null;
} catch (Exception e) {
Log.e(TAG, "Ошибка при инициализации MapForge: " + e.getMessage());
return null;
}
}
/**
* Запускает карту
*/
public void startMap() {
if (mapView != null) {
mapView.onStart();
}
if (isYandexMapsInitialized) {
com.yandex.mapkit.MapKitFactory.getInstance().onStart();
}
}
/**
* Останавливает карту
*/
public void stopMap() {
if (mapView != null) {
mapView.onStop();
}
if (isYandexMapsInitialized) {
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
}
}
/**
* Получает текущий интерфейс карты
*/
public MapInterface getCurrentMapInterface() {
return currentMapInterface;
}
/**
* Устанавливает флаг инициализации Яндекс.Карт
*/
public static void setYandexMapsInitialized(boolean initialized) {
isYandexMapsInitialized = initialized;
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
if (currentMapInterface != null) {
currentMapInterface.cleanup();
}
if (mapView != null) {
mapView.onStop();
}
if (isYandexMapsInitialized) {
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,168 @@
package com.grigowashere.aismap.controllers;
import android.util.Log;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Контроллер для прослушивания UDP портов
*/
public class UDPListener {
private static final String TAG = "UDPListener";
private static final int BUFFER_SIZE = 1024;
private int port;
private DatagramSocket socket;
private ExecutorService executor;
private AtomicBoolean isRunning;
private UDPListenerCallback callback;
public interface UDPListenerCallback {
void onDataReceived(String data, String sourceAddress, int sourcePort);
void onUDPError(String error);
}
public UDPListener(int port) {
this.port = port;
this.executor = Executors.newSingleThreadExecutor();
this.isRunning = new AtomicBoolean(false);
}
public void setCallback(UDPListenerCallback callback) {
this.callback = callback;
}
/**
* Запускает прослушивание UDP порта
*/
public void start() {
if (isRunning.get()) {
Log.w(TAG, "UDP слушатель уже запущен");
return;
}
executor.execute(() -> {
try {
socket = new DatagramSocket(port);
isRunning.set(true);
Log.i(TAG, "UDP слушатель запущен на порту " + port);
while (isRunning.get()) {
byte[] buffer = new byte[BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
socket.receive(packet);
String data = new String(packet.getData(), 0, packet.getLength());
String sourceAddress = packet.getAddress().getHostAddress();
int sourcePort = packet.getPort();
if (callback != null) {
callback.onDataReceived(data, sourceAddress, sourcePort);
}
Log.d(TAG, "Получены данные от " + sourceAddress + ":" + sourcePort + ": " + data);
} catch (IOException e) {
if (isRunning.get()) {
Log.e(TAG, "Ошибка при получении UDP пакета: " + e.getMessage());
if (callback != null) {
callback.onUDPError("Ошибка UDP: " + e.getMessage());
}
}
}
}
} catch (IOException e) {
Log.e(TAG, "Ошибка при создании UDP сокета: " + e.getMessage());
if (callback != null) {
callback.onUDPError("Не удалось создать UDP сокет: " + e.getMessage());
}
} finally {
stop();
}
});
}
/**
* Останавливает прослушивание UDP порта
*/
public void stop() {
isRunning.set(false);
if (socket != null && !socket.isClosed()) {
socket.close();
socket = null;
}
Log.i(TAG, "UDP слушатель остановлен");
}
/**
* Отправляет UDP пакет
*/
public void sendData(String data, String targetAddress, int targetPort) {
if (socket == null || socket.isClosed()) {
Log.w(TAG, "UDP сокет не создан");
return;
}
executor.execute(() -> {
try {
byte[] buffer = data.getBytes();
InetAddress address = InetAddress.getByName(targetAddress);
DatagramPacket packet = new DatagramPacket(buffer, buffer.length, address, targetPort);
socket.send(packet);
Log.d(TAG, "Отправлены данные на " + targetAddress + ":" + targetPort + ": " + data);
} catch (IOException e) {
Log.e(TAG, "Ошибка при отправке UDP пакета: " + e.getMessage());
if (callback != null) {
callback.onUDPError("Ошибка отправки UDP: " + e.getMessage());
}
}
});
}
/**
* Проверяет, запущен ли слушатель
*/
public boolean isRunning() {
return isRunning.get();
}
/**
* Получает текущий порт
*/
public int getPort() {
return port;
}
/**
* Устанавливает новый порт
*/
public void setPort(int port) {
if (isRunning.get()) {
Log.w(TAG, "Нельзя изменить порт во время работы");
return;
}
this.port = port;
}
/**
* Освобождает ресурсы
*/
public void cleanup() {
stop();
if (executor != null && !executor.isShutdown()) {
executor.shutdown();
}
}
}
@@ -0,0 +1,157 @@
package com.grigowashere.aismap.maps;
import android.content.Context;
import android.graphics.Color;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import org.mapsforge.core.model.LatLong;
import org.mapsforge.map.android.view.MapView;
import org.mapsforge.map.layer.Layers;
import org.mapsforge.map.layer.overlay.Marker;
import org.mapsforge.map.model.Model;
import org.mapsforge.core.graphics.Bitmap;
import java.util.HashMap;
import java.util.Map;
/**
* Реализация карты для MapForge
*/
public class MapForgeImpl implements MapInterface {
private Context context;
private MapView mapView;
private Layers layers;
private MarkerClickListener markerClickListener;
private Map<String, Marker> aisMarkers;
private Marker ownVesselMarker;
public MapForgeImpl(Context context, MapView mapView) {
this.context = context;
this.mapView = mapView;
this.aisMarkers = new HashMap<>();
this.layers = mapView.getLayerManager().getLayers();
}
@Override
public void initialize() {
// MapForge уже инициализирован
}
@Override
public void cleanup() {
if (mapView != null) {
mapView.destroy();
}
}
@Override
public void addOwnVesselMarker(Vessel vessel) {
if (ownVesselMarker != null) {
layers.remove(ownVesselMarker);
}
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.BLUE, vessel.getCourse());
ownVesselMarker = new Marker(position, icon, 0, 0);
// MapForge не поддерживает OnTapListener напрямую, нужно использовать другой подход
layers.add(ownVesselMarker);
}
@Override
public void updateOwnVesselPosition(Vessel vessel) {
if (ownVesselMarker != null) {
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
ownVesselMarker.setLatLong(newPosition);
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.BLUE, vessel.getCourse());
ownVesselMarker.setBitmap(icon);
}
}
@Override
public void addAISVesselMarker(AISVessel vessel) {
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
Marker marker = new Marker(position, icon, 0, 0);
// MapForge не поддерживает OnTapListener напрямую
layers.add(marker);
aisMarkers.put(vessel.getMmsi(), marker);
}
@Override
public void updateAISVesselPosition(AISVessel vessel) {
Marker marker = aisMarkers.get(vessel.getMmsi());
if (marker != null) {
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
marker.setLatLong(newPosition);
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
marker.setBitmap(icon);
}
}
@Override
public void removeAISVesselMarker(String mmsi) {
Marker marker = aisMarkers.remove(mmsi);
if (marker != null) {
layers.remove(marker);
}
}
@Override
public void clearAISVesselMarkers() {
for (Marker marker : aisMarkers.values()) {
layers.remove(marker);
}
aisMarkers.clear();
}
@Override
public void centerOnPosition(double latitude, double longitude) {
LatLong position = new LatLong(latitude, longitude);
mapView.getModel().mapViewPosition.setCenter(position);
}
@Override
public void setZoom(float zoom) {
mapView.getModel().mapViewPosition.setZoomLevel((byte) zoom);
}
@Override
public float getZoom() {
return mapView.getModel().mapViewPosition.getZoomLevel();
}
@Override
public void addLayer(String layerId, Object layerData) {
// Реализация добавления слоев для MapForge
}
@Override
public void removeLayer(String layerId) {
// Реализация удаления слоев
}
@Override
public void setMarkerClickListener(MarkerClickListener listener) {
this.markerClickListener = listener;
}
private org.mapsforge.core.graphics.Bitmap createMapForgeIcon(int color, double course) {
// Создаем простую иконку для MapForge
// В реальном приложении нужно конвертировать Android Bitmap в MapForge Bitmap
// Пока возвращаем null - это заглушка
return null;
}
public MapView getMapView() {
return mapView;
}
}
@@ -0,0 +1,89 @@
package com.grigowashere.aismap.maps;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
/**
* Интерфейс для работы с картами
* Позволяет использовать разные SDK карт
*/
public interface MapInterface {
/**
* Инициализация карты
*/
void initialize();
/**
* Очистка ресурсов карты
*/
void cleanup();
/**
* Добавление метки нашего судна
*/
void addOwnVesselMarker(Vessel vessel);
/**
* Обновление позиции нашего судна
*/
void updateOwnVesselPosition(Vessel vessel);
/**
* Добавление метки AIS судна
*/
void addAISVesselMarker(AISVessel vessel);
/**
* Обновление позиции AIS судна
*/
void updateAISVesselPosition(AISVessel vessel);
/**
* Удаление метки AIS судна
*/
void removeAISVesselMarker(String mmsi);
/**
* Очистка всех AIS меток
*/
void clearAISVesselMarkers();
/**
* Центрирование карты на позиции
*/
void centerOnPosition(double latitude, double longitude);
/**
* Установка зума карты
*/
void setZoom(float zoom);
/**
* Получение текущего зума
*/
float getZoom();
/**
* Добавление дополнительного слоя
*/
void addLayer(String layerId, Object layerData);
/**
* Удаление слоя
*/
void removeLayer(String layerId);
/**
* Установка обработчика кликов по меткам
*/
void setMarkerClickListener(MarkerClickListener listener);
/**
* Интерфейс для обработки кликов по меткам
*/
interface MarkerClickListener {
void onOwnVesselClick(Vessel vessel);
void onAISVesselClick(AISVessel vessel);
}
}
@@ -0,0 +1,533 @@
package com.grigowashere.aismap.maps;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.view.View;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.models.AISVessel;
import com.yandex.mapkit.Animation;
import com.yandex.mapkit.geometry.Point;
import com.yandex.mapkit.map.CameraPosition;
import com.yandex.mapkit.map.MapObjectCollection;
import com.yandex.mapkit.mapview.MapView;
import com.yandex.runtime.image.ImageProvider;
import java.util.HashMap;
import java.util.Map;
/**
* Реализация карты для Яндекс.Карт
*/
public class YandexMapImpl implements MapInterface {
private Context context;
private MapView mapView;
private MapObjectCollection mapObjects;
private MarkerClickListener markerClickListener;
private Map<String, com.yandex.mapkit.map.PlacemarkMapObject> aisMarkers;
private Map<String, AISVessel> aisVessels; // Храним ссылки на AISVessel объекты
private com.yandex.mapkit.map.PlacemarkMapObject ownVesselMarker;
private Vessel ownVessel; // Храним ссылку на наше судно
// Флаги для отслеживания состояния обработчиков
private boolean ownVesselClickListenerSet = false;
private Map<String, Boolean> aisVesselClickListenersSet = new HashMap<>();
public YandexMapImpl(Context context, MapView mapView) {
this.context = context;
this.mapView = mapView;
this.aisMarkers = new HashMap<>();
this.aisVessels = new HashMap<>();
android.util.Log.d("YandexMapImpl", "Конструктор YandexMapImpl вызван");
android.util.Log.d("YandexMapImpl", "Context: " + (context != null ? "установлен" : "null"));
android.util.Log.d("YandexMapImpl", "MapView: " + (mapView != null ? "установлен" : "null"));
// Получение коллекции объектов карты
try {
this.mapObjects = mapView.getMap().getMapObjects().addCollection();
android.util.Log.d("YandexMapImpl", "Коллекция объектов карты создана: " + (mapObjects != null ? "успешно" : "null"));
} catch (Exception e) {
android.util.Log.e("YandexMapImpl", "Ошибка создания коллекции объектов карты: " + e.getMessage(), e);
}
}
@Override
public void initialize() {
android.util.Log.d("YandexMapImpl", "initialize() вызван");
android.util.Log.d("YandexMapImpl", "mapObjects: " + (mapObjects != null ? "установлен" : "null"));
android.util.Log.d("YandexMapImpl", "mapView: " + (mapView != null ? "установлен" : "null"));
android.util.Log.d("YandexMapImpl", "context: " + (context != null ? "установлен" : "null"));
// Карта уже инициализирована в конструкторе
if (mapObjects != null) {
android.util.Log.d("YandexMapImpl", "Коллекция объектов карты готова к использованию");
}
}
@Override
public void cleanup() {
if (mapObjects != null) {
mapView.getMap().getMapObjects().remove(mapObjects);
}
if (mapView != null) {
mapView.onStop();
}
}
@Override
public void addOwnVesselMarker(Vessel vessel) {
android.util.Log.d("YandexMapImpl", "addOwnVesselMarker вызван: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", course=" + vessel.getCourse() + "°");
// Сохраняем ссылку на судно
this.ownVessel = vessel;
// Проверяем координаты
if (vessel.getLatitude() == 0.0 && vessel.getLongitude() == 0.0) {
android.util.Log.w("YandexMapImpl", "Координаты равны 0,0 - маркер не будет создан");
return;
}
if (ownVesselMarker != null) {
android.util.Log.d("YandexMapImpl", "Удаляем существующий маркер");
mapObjects.remove(ownVesselMarker);
}
Point point = new Point(vessel.getLatitude(), vessel.getLongitude());
android.util.Log.d("YandexMapImpl", "Создаем Point: " + point);
ownVesselMarker = mapObjects.addPlacemark(point);
android.util.Log.d("YandexMapImpl", "Placemark создан: " + (ownVesselMarker != null ? "успешно" : "null"));
if (ownVesselMarker == null) {
android.util.Log.e("YandexMapImpl", "Не удалось создать Placemark!");
return;
}
// Используем готовую иконку стрелки с учетом курса
android.util.Log.d("YandexMapImpl", "Устанавливаем иконку стрелки с курсом: " + vessel.getCourse() + "°");
setMarkerIcon(ownVesselMarker, "arrowship", vessel.getCourse());
// Устанавливаем размер иконки
android.util.Log.d("YandexMapImpl", "Устанавливаем IconStyle...");
com.yandex.mapkit.map.IconStyle iconStyle = new com.yandex.mapkit.map.IconStyle();
iconStyle.setScale(1.5f); // Увеличиваем размер иконки
ownVesselMarker.setIconStyle(iconStyle);
// Устанавливаем обработчик кликов только если он еще не установлен
if (!ownVesselClickListenerSet) {
android.util.Log.d("YandexMapImpl", "Устанавливаем обработчик клика для маркера...");
ownVesselMarker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
if (markerClickListener != null && ownVessel != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
markerClickListener.onOwnVesselClick(ownVessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
android.util.Log.d("YandexMapImpl", "markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
android.util.Log.d("YandexMapImpl", "ownVessel = " + (ownVessel != null ? "установлен" : "null"));
}
return true;
});
ownVesselClickListenerSet = true;
}
android.util.Log.d("YandexMapImpl", "Маркер нашего судна создан и настроен, markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
// Проверяем, что маркер действительно добавлен в коллекцию
android.util.Log.d("YandexMapImpl", "Маркер добавлен в коллекцию объектов карты");
}
@Override
public void updateOwnVesselPosition(Vessel vessel) {
android.util.Log.d("YandexMapImpl", "updateOwnVesselPosition вызван: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", course=" + vessel.getCourse() + "°");
// Обновляем ссылку на судно
this.ownVessel = vessel;
// Проверяем координаты
if (vessel.getLatitude() == 0.0 && vessel.getLongitude() == 0.0) {
android.util.Log.w("YandexMapImpl", "Координаты равны 0,0 - обновление пропущено");
return;
}
if (ownVesselMarker == null) {
// Создаем маркер нашего судна, если его еще нет
android.util.Log.d("YandexMapImpl", "Создаем новый маркер нашего судна");
addOwnVesselMarker(vessel);
} else {
// Проверяем, нужно ли обновить курс
boolean needCourseUpdate = Math.abs(vessel.getCourse()) > 0.1; // Если курс больше 0.1 градуса
if (needCourseUpdate) {
android.util.Log.d("YandexMapImpl", "Обновляем курс маркера на " + vessel.getCourse() + "°");
// Обновляем только иконку с новым курсом
setMarkerIcon(ownVesselMarker, "arrowship", vessel.getCourse());
}
// Обновляем позицию маркера
Point newPoint = new Point(vessel.getLatitude(), vessel.getLongitude());
ownVesselMarker.setGeometry(newPoint);
android.util.Log.d("YandexMapImpl", "Позиция маркера обновлена на: " + newPoint);
// Переустанавливаем обработчик клика после обновления маркера
if (markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика после обновления маркера");
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
ownVesselMarker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
if (markerClickListener != null && ownVessel != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
markerClickListener.onOwnVesselClick(ownVessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
}
return true;
});
}
}
android.util.Log.d("YandexMapImpl", "Маркер нашего судна обновлен, ownVesselMarker = " + (ownVesselMarker != null ? "создан" : "null") + ", markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
}
@Override
public void addAISVesselMarker(AISVessel vessel) {
android.util.Log.d("YandexMapImpl", "addAISVesselMarker вызван: lat=" + vessel.getLatitude() + ", lon=" + vessel.getLongitude() + ", course=" + vessel.getCourse() + "°");
Point point = new Point(vessel.getLatitude(), vessel.getLongitude());
com.yandex.mapkit.map.PlacemarkMapObject marker = mapObjects.addPlacemark(point);
// Сохраняем ссылку на судно
aisVessels.put(vessel.getMmsi(), vessel);
// Используем готовую иконку стрелки для AIS судов с учетом курса
setMarkerIcon(marker, "arrowship", vessel.getCourse());
// Устанавливаем размер иконки
com.yandex.mapkit.map.IconStyle iconStyle = new com.yandex.mapkit.map.IconStyle();
iconStyle.setScale(1.5f); // Увеличиваем размер иконки
marker.setIconStyle(iconStyle);
// Установка обработчика кликов только если он еще не установлен
String mmsi = vessel.getMmsi();
if (!aisVesselClickListenersSet.containsKey(mmsi) || !aisVesselClickListenersSet.get(mmsi)) {
marker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + mmsi);
if (markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
markerClickListener.onAISVesselClick(vessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
android.util.Log.d("YandexMapImpl", "markerClickListener = " + (markerClickListener != null ? "установлен" : "null"));
}
return true;
});
aisVesselClickListenersSet.put(mmsi, true);
}
aisMarkers.put(vessel.getMmsi(), marker);
}
@Override
public void updateAISVesselPosition(AISVessel vessel) {
// Обновляем ссылку на судно
aisVessels.put(vessel.getMmsi(), vessel);
com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.get(vessel.getMmsi());
if (marker != null) {
Point newPoint = new Point(vessel.getLatitude(), vessel.getLongitude());
marker.setGeometry(newPoint);
// Обновляем курс маркера, если он изменился
if (Math.abs(vessel.getCourse()) > 0.1) {
android.util.Log.d("YandexMapImpl", "Обновляем курс AIS маркера " + vessel.getMmsi() + " на " + vessel.getCourse() + "°");
setMarkerIcon(marker, "arrowship", vessel.getCourse());
}
// Переустанавливаем обработчик клика после обновления маркера
if (markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика для AIS маркера: " + vessel.getMmsi());
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
marker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + vessel.getMmsi());
if (markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
markerClickListener.onAISVesselClick(vessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
}
return true;
});
}
}
}
@Override
public void removeAISVesselMarker(String mmsi) {
com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.remove(mmsi);
if (marker != null) {
mapObjects.remove(marker);
}
// Удаляем ссылку на судно
aisVessels.remove(mmsi);
// Удаляем флаг обработчика кликов
aisVesselClickListenersSet.remove(mmsi);
}
@Override
public void clearAISVesselMarkers() {
for (com.yandex.mapkit.map.PlacemarkMapObject marker : aisMarkers.values()) {
mapObjects.remove(marker);
}
aisMarkers.clear();
aisVessels.clear();
aisVesselClickListenersSet.clear();
}
@Override
public void centerOnPosition(double latitude, double longitude) {
Point point = new Point(latitude, longitude);
CameraPosition cameraPosition = new CameraPosition(point, 15.0f, 0.0f, 0.0f);
mapView.getMap().move(cameraPosition, new Animation(Animation.Type.SMOOTH, 1.0f), null);
}
@Override
public void setZoom(float zoom) {
CameraPosition currentPosition = mapView.getMap().getCameraPosition();
Point target = currentPosition.getTarget();
CameraPosition newPosition = new CameraPosition(target, zoom, currentPosition.getAzimuth(), currentPosition.getTilt());
mapView.getMap().move(newPosition, new Animation(Animation.Type.SMOOTH, 0.5f), null);
}
@Override
public float getZoom() {
return mapView.getMap().getCameraPosition().getZoom();
}
@Override
public void addLayer(String layerId, Object layerData) {
// Реализация добавления дополнительных слоев
// Зависит от конкретных требований
}
@Override
public void removeLayer(String layerId) {
// Реализация удаления слоев
}
@Override
public void setMarkerClickListener(MarkerClickListener listener) {
android.util.Log.d("YandexMapImpl", "setMarkerClickListener вызван: " + (listener != null ? "listener установлен" : "listener == null"));
this.markerClickListener = listener;
// Переустанавливаем обработчики кликов для всех существующих маркеров
updateAllMarkerClickListeners();
}
/**
* Обновляет обработчики кликов для всех существующих маркеров
* Этот метод переустанавливает обработчики для всех маркеров
*/
private void updateAllMarkerClickListeners() {
android.util.Log.d("YandexMapImpl", "updateAllMarkerClickListeners вызван - переустанавливаем обработчики");
// Переустанавливаем обработчик для маркера нашего судна
if (ownVesselMarker != null) {
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик для маркера нашего судна");
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
ownVesselMarker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
if (markerClickListener != null && ownVessel != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
markerClickListener.onOwnVesselClick(ownVessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
}
return true;
});
ownVesselClickListenerSet = true;
}
// Переустанавливаем обработчики для AIS маркеров
for (Map.Entry<String, com.yandex.mapkit.map.PlacemarkMapObject> entry : aisMarkers.entrySet()) {
String mmsi = entry.getKey();
com.yandex.mapkit.map.PlacemarkMapObject marker = entry.getValue();
AISVessel vessel = aisVessels.get(mmsi);
if (marker != null && vessel != null) {
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик для AIS маркера: " + mmsi);
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
marker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + mmsi);
if (markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
markerClickListener.onAISVesselClick(vessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
}
return true;
});
aisVesselClickListenersSet.put(mmsi, true);
}
}
}
/**
* Создание иконки судна
*/
private Bitmap createVesselIcon(int color, double course) {
try {
int size = 64; // Увеличиваем размер для лучшей видимости
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(Paint.Style.FILL);
paint.setAntiAlias(true);
paint.setStrokeWidth(3.0f);
// Рисуем треугольник-стрелку, направленную вверх (по умолчанию)
android.graphics.Path path = new android.graphics.Path();
path.moveTo(size / 2f, 0); // вершина
path.lineTo(size * 0.1f, size * 0.8f); // левый нижний угол
path.lineTo(size * 0.3f, size * 0.6f); // левая внутренняя точка
path.lineTo(size * 0.3f, size * 0.9f); // левая нижняя точка
path.lineTo(size * 0.7f, size * 0.9f); // правая нижняя точка
path.lineTo(size * 0.7f, size * 0.6f); // правая внутренняя точка
path.lineTo(size * 0.9f, size * 0.8f); // правый нижний угол
path.close();
// Поворачиваем стрелку на курс (курс 0° = стрелка направлена вверх)
// В морской навигации курс 0° = север, 90° = восток, 180° = юг, 270° = запад
canvas.save();
canvas.rotate((float) course, size / 2f, size / 2f);
canvas.drawPath(path, paint);
canvas.restore();
android.util.Log.d("YandexMapImpl", "Программная иконка с курсом " + course + "° создана успешно, размер: " + size + "x" + size);
return bitmap;
} catch (Exception e) {
android.util.Log.e("YandexMapImpl", "Ошибка создания программной иконки: " + e.getMessage(), e);
return null;
}
}
/**
* Получение MapView для использования в layout
*/
public MapView getMapView() {
return mapView;
}
/**
* Принудительно пересоздает маркер нашего судна с иконкой
*/
public void recreateOwnVesselMarker(Vessel vessel) {
android.util.Log.d("YandexMapImpl", "Принудительно пересоздаем маркер нашего судна");
if (ownVesselMarker != null) {
mapObjects.remove(ownVesselMarker);
ownVesselMarker = null;
}
addOwnVesselMarker(vessel);
}
/**
* Обновляет обработчики кликов для всех маркеров
* Вызывается после закрытия BottomSheet для восстановления функциональности
*/
public void refreshMarkerClickListeners() {
android.util.Log.d("YandexMapImpl", "refreshMarkerClickListeners вызван - переустанавливаем все обработчики");
updateAllMarkerClickListeners();
}
/**
* Устанавливает иконку для маркера с fallback
*/
private void setMarkerIcon(com.yandex.mapkit.map.PlacemarkMapObject marker, String iconName, double course) {
try {
android.util.Log.d("YandexMapImpl", "Пытаемся установить иконку: " + iconName + " с курсом: " + course + "°");
android.util.Log.d("YandexMapImpl", "Package name: " + context.getPackageName());
// Сначала пробуем создать программную иконку с учетом курса
android.util.Log.d("YandexMapImpl", "Создаем программную иконку стрелки с курсом " + course + "°...");
Bitmap iconBitmap = createVesselIcon(android.graphics.Color.BLUE, course);
if (iconBitmap != null) {
android.util.Log.d("YandexMapImpl", "Программная иконка с курсом " + course + "° создана, устанавливаем...");
marker.setIcon(ImageProvider.fromBitmap(iconBitmap));
android.util.Log.d("YandexMapImpl", "Программная иконка с курсом " + course + "° установлена успешно");
return;
}
// Если программная иконка не создалась, пробуем ресурс
int iconResId = context.getResources().getIdentifier(iconName, "drawable", context.getPackageName());
android.util.Log.d("YandexMapImpl", "ID ресурса " + iconName + ": " + iconResId);
if (iconResId != 0) {
android.util.Log.d("YandexMapImpl", "Устанавливаем иконку из ресурса...");
marker.setIcon(ImageProvider.fromResource(context, iconResId));
android.util.Log.d("YandexMapImpl", "Иконка " + iconName + " установлена успешно");
} else {
android.util.Log.e("YandexMapImpl", "Не удалось найти ресурс " + iconName);
android.util.Log.d("YandexMapImpl", "Используем fallback иконку...");
// Создаем простую иконку как fallback
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
android.util.Log.d("YandexMapImpl", "Fallback иконка установлена");
}
} catch (Exception e) {
android.util.Log.e("YandexMapImpl", "Ошибка установки иконки " + iconName + ": " + e.getMessage(), e);
android.util.Log.d("YandexMapImpl", "Используем fallback иконку после ошибки...");
// Создаем простую иконку как fallback
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
android.util.Log.d("YandexMapImpl", "Fallback иконка установлена после ошибки");
}
// После установки иконки проверяем, что обработчик клика все еще работает
// Это может помочь с проблемами, когда установка иконки нарушает обработчики
android.util.Log.d("YandexMapImpl", "Иконка установлена, проверяем обработчик клика...");
// Дополнительная проверка: если это маркер нашего судна, переустанавливаем обработчик клика
if (marker == ownVesselMarker && markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика для маркера нашего судна после установки иконки");
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
marker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по маркеру нашего судна!");
if (markerClickListener != null && ownVessel != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onOwnVesselClick");
markerClickListener.onOwnVesselClick(ownVessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null или ownVessel == null!");
}
return true;
});
}
// Дополнительная проверка: если это AIS маркер, переустанавливаем обработчик клика
for (Map.Entry<String, com.yandex.mapkit.map.PlacemarkMapObject> entry : aisMarkers.entrySet()) {
if (entry.getValue() == marker && markerClickListener != null) {
String mmsi = entry.getKey();
AISVessel vessel = aisVessels.get(mmsi);
if (vessel != null) {
android.util.Log.d("YandexMapImpl", "Переустанавливаем обработчик клика для AIS маркера " + mmsi + " после установки иконки");
// В Яндекс.Картах нет метода setTapListener(null), поэтому просто добавляем новый обработчик
marker.addTapListener((mapObject, point1) -> {
android.util.Log.d("YandexMapImpl", "Клик по AIS маркеру: " + mmsi);
if (markerClickListener != null) {
android.util.Log.d("YandexMapImpl", "Вызываем callback onAISVesselClick");
markerClickListener.onAISVesselClick(vessel);
} else {
android.util.Log.e("YandexMapImpl", "markerClickListener == null!");
}
return true;
});
}
break;
}
}
}
}
@@ -0,0 +1,142 @@
package com.grigowashere.aismap.models;
import java.time.LocalDateTime;
/**
* Модель AIS судна
*/
public class AISVessel {
private String mmsi; // Maritime Mobile Service Identity
private String vesselName; // название судна
private String callSign; // позывной
private int imo; // IMO номер
private String vesselType; // тип судна
private double latitude;
private double longitude;
private double course; // курс в градусах (0-360)
private double speed; // скорость в узлах
private double heading; // направление движения в градусах
private double length; // длина судна в метрах
private double width; // ширина судна в метрах
private double draft; // осадка в метрах
private String destination; // пункт назначения
private LocalDateTime eta; // предполагаемое время прибытия
private LocalDateTime lastUpdate;
private int signalStrength; // сила AIS сигнала
private boolean isActive; // активно ли судно
private String navigationalStatus; // навигационный статус
private String lastSafetyMessage; // последнее сообщение безопасности
private boolean positionAccuracy; // точность позиции
private String vesselClass; // класс судна (Class A, Class B, Extended Class B)
private String vendorId; // идентификатор производителя оборудования
public AISVessel() {
this.lastUpdate = LocalDateTime.now();
this.isActive = true;
}
public AISVessel(String mmsi) {
this();
this.mmsi = mmsi;
}
// Геттеры и сеттеры
public String getMmsi() { return mmsi; }
public void setMmsi(String mmsi) { this.mmsi = mmsi; }
public String getVesselName() { return vesselName; }
public void setVesselName(String vesselName) { this.vesselName = vesselName; }
public String getCallSign() { return callSign; }
public void setCallSign(String callSign) { this.callSign = callSign; }
public int getImo() { return imo; }
public void setImo(int imo) { this.imo = imo; }
public String getVesselType() { return vesselType; }
public void setVesselType(String vesselType) { this.vesselType = vesselType; }
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public double getCourse() { return course; }
public void setCourse(double course) { this.course = course; }
public double getSpeed() { return speed; }
public void setSpeed(double speed) { this.speed = speed; }
public double getHeading() { return heading; }
public void setHeading(double heading) { this.heading = heading; }
public double getLength() { return length; }
public void setLength(double length) { this.length = length; }
public double getWidth() { return width; }
public void setWidth(double width) { this.width = width; }
public double getDraft() { return draft; }
public void setDraft(double draft) { this.draft = draft; }
public String getDestination() { return destination; }
public void setDestination(String destination) { this.destination = destination; }
public LocalDateTime getEta() { return eta; }
public void setEta(LocalDateTime eta) { this.eta = eta; }
public LocalDateTime getLastUpdate() { return lastUpdate; }
public void setLastUpdate(LocalDateTime lastUpdate) { this.lastUpdate = lastUpdate; }
public int getSignalStrength() { return signalStrength; }
public void setSignalStrength(int signalStrength) { this.signalStrength = signalStrength; }
public boolean isActive() { return isActive; }
public void setActive(boolean active) { isActive = active; }
public String getNavigationalStatus() { return navigationalStatus; }
public void setNavigationalStatus(String navigationalStatus) { this.navigationalStatus = navigationalStatus; }
public String getLastSafetyMessage() { return lastSafetyMessage; }
public void setLastSafetyMessage(String lastSafetyMessage) { this.lastSafetyMessage = lastSafetyMessage; }
public boolean isPositionAccuracy() { return positionAccuracy; }
public void setPositionAccuracy(boolean positionAccuracy) { this.positionAccuracy = positionAccuracy; }
public String getVesselClass() { return vesselClass; }
public void setVesselClass(String vesselClass) { this.vesselClass = vesselClass; }
public String getVendorId() { return vendorId; }
public void setVendorId(String vendorId) { this.vendorId = vendorId; }
/**
* Обновляет позицию и курс судна
*/
public void updatePosition(double latitude, double longitude, double course, double speed) {
this.latitude = latitude;
this.longitude = longitude;
this.course = course;
this.speed = speed;
this.lastUpdate = LocalDateTime.now();
}
/**
* Проверяет, не устарели ли данные (больше 10 минут)
*/
public boolean isDataStale() {
return LocalDateTime.now().minusMinutes(10).isAfter(lastUpdate);
}
@Override
public String toString() {
return "AISVessel{" +
"mmsi='" + mmsi + '\'' +
", name='" + vesselName + '\'' +
", lat=" + latitude +
", lon=" + longitude +
", course=" + course +
", speed=" + speed +
'}';
}
}
@@ -0,0 +1,170 @@
package com.grigowashere.aismap.models;
import java.time.LocalDateTime;
/**
* Модель нашего судна
*/
public class Vessel {
private double latitude;
private double longitude;
private double course; // курс в градусах (0-360)
private double speed; // скорость в узлах
private double heading; // направление движения в градусах
private double magneticCompass; // магнитный компас в градусах (0-360)
private int signalStrength; // сила сигнала GPS (0-100)
private LocalDateTime lastUpdate;
private String vesselName;
private String mmsi; // Maritime Mobile Service Identity
private String callSign; // позывной
private double altitude; // высота над уровнем моря
private int satellites; // общее количество спутников
private int activeSatellites; // количество активных спутников в фиксе
// DOP (Dilution of Precision) - показатели качества GPS
private double pdop; // Position DOP - общая точность позиции
private double hdop; // Horizontal DOP - точность по горизонтали
private double vdop; // Vertical DOP - точность по вертикали
// Дополнительные GPS параметры
private float accuracy; // точность в метрах
private long fixTime; // время последнего фикса
private String fixQuality; // качество фикса (GPS, DGPS, RTK и т.д.)
public Vessel() {
this.lastUpdate = LocalDateTime.now();
this.fixQuality = "NO_FIX";
this.accuracy = -1.0f;
}
public Vessel(double latitude, double longitude) {
this();
this.latitude = latitude;
this.longitude = longitude;
}
// Геттеры и сеттеры
public double getLatitude() { return latitude; }
public void setLatitude(double latitude) { this.latitude = latitude; }
public double getLongitude() { return longitude; }
public void setLongitude(double longitude) { this.longitude = longitude; }
public double getCourse() { return course; }
public void setCourse(double course) { this.course = course; }
public double getSpeed() { return speed; }
public void setSpeed(double speed) { this.speed = speed; }
public double getHeading() { return heading; }
public void setHeading(double heading) { this.heading = heading; }
public double getMagneticCompass() { return magneticCompass; }
public void setMagneticCompass(double magneticCompass) { this.magneticCompass = magneticCompass; }
public int getSignalStrength() { return signalStrength; }
public void setSignalStrength(int signalStrength) { this.signalStrength = signalStrength; }
public LocalDateTime getLastUpdate() { return lastUpdate; }
public void setLastUpdate(LocalDateTime lastUpdate) { this.lastUpdate = lastUpdate; }
public String getVesselName() { return vesselName; }
public void setVesselName(String vesselName) { this.vesselName = vesselName; }
public String getMmsi() { return mmsi; }
public void setMmsi(String mmsi) { this.mmsi = mmsi; }
public String getCallSign() { return callSign; }
public void setCallSign(String callSign) { this.callSign = callSign; }
public double getAltitude() { return altitude; }
public void setAltitude(double altitude) { this.altitude = altitude; }
public int getSatellites() { return satellites; }
public void setSatellites(int satellites) { this.satellites = satellites; }
public int getActiveSatellites() { return activeSatellites; }
public void setActiveSatellites(int activeSatellites) { this.activeSatellites = activeSatellites; }
public double getPdop() { return pdop; }
public void setPdop(double pdop) { this.pdop = pdop; }
public double getHdop() { return hdop; }
public void setHdop(double hdop) { this.hdop = hdop; }
public double getVdop() { return vdop; }
public void setVdop(double vdop) { this.vdop = vdop; }
public float getAccuracy() { return accuracy; }
public void setAccuracy(float accuracy) { this.accuracy = accuracy; }
public long getFixTime() { return fixTime; }
public void setFixTime(long fixTime) { this.fixTime = fixTime; }
public String getFixQuality() { return fixQuality; }
public void setFixQuality(String fixQuality) { this.fixQuality = fixQuality; }
/**
* Обновляет данные судна
*/
public void updatePosition(double latitude, double longitude, double course, double speed) {
this.latitude = latitude;
this.longitude = longitude;
this.course = course;
this.speed = speed;
this.lastUpdate = LocalDateTime.now();
}
/**
* Обновляет GPS качество
*/
public void updateGPSQuality(int satellites, int activeSatellites, double pdop, double hdop, double vdop, float accuracy) {
this.satellites = satellites;
this.activeSatellites = activeSatellites;
this.pdop = pdop;
this.hdop = hdop;
this.vdop = vdop;
this.accuracy = accuracy;
this.lastUpdate = LocalDateTime.now();
}
/**
* Получает качество GPS сигнала в процентах
*/
public int getGPSQualityPercentage() {
if (accuracy <= 0) return 0;
if (accuracy <= 3) return 100; // Отличное качество (≤3м)
if (accuracy <= 5) return 90; // Очень хорошее (≤5м)
if (accuracy <= 10) return 80; // Хорошее (≤10м)
if (accuracy <= 20) return 60; // Удовлетворительное (≤20м)
if (accuracy <= 50) return 40; // Плохое (≤50м)
return 20; // Очень плохое (>50м)
}
/**
* Получает текстовое описание качества GPS
*/
public String getGPSQualityDescription() {
int quality = getGPSQualityPercentage();
if (quality >= 90) return "Отличное";
if (quality >= 80) return "Очень хорошее";
if (quality >= 60) return "Хорошее";
if (quality >= 40) return "Удовлетворительное";
if (quality >= 20) return "Плохое";
return "Очень плохое";
}
@Override
public String toString() {
return "Vessel{" +
"lat=" + latitude +
", lon=" + longitude +
", course=" + course +
", speed=" + speed +
", name='" + vesselName + '\'' +
", satellites=" + satellites + "/" + activeSatellites +
", accuracy=" + accuracy + "m" +
", quality=" + getGPSQualityDescription() +
'}';
}
}
@@ -0,0 +1,158 @@
package com.grigowashere.aismap.sensors;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.util.Log;
/**
* Класс для работы с магнитным компасом устройства
*/
public class CompassSensor implements SensorEventListener {
private static final String TAG = "CompassSensor";
private SensorManager sensorManager;
private Sensor accelerometer;
private Sensor magnetometer;
private float[] accelerometerReading = new float[3];
private float[] magnetometerReading = new float[3];
private float[] rotationMatrix = new float[9];
private float[] orientationAngles = new float[3];
private CompassListener compassListener;
private boolean isListening = false;
// Скользящий фильтр для сглаживания значений
private static final int FILTER_SIZE = 60;
private float[] azimuthBuffer = new float[FILTER_SIZE];
private int bufferIndex = 0;
private boolean bufferFull = false;
public interface CompassListener {
void onCompassChanged(float azimuth);
}
public CompassSensor(Context context) {
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magnetometer = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
}
public void startListening(CompassListener listener) {
if (isListening) {
stopListening();
}
this.compassListener = listener;
if (accelerometer != null && magnetometer != null) {
sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
sensorManager.registerListener(this, magnetometer, SensorManager.SENSOR_DELAY_GAME);
isListening = true;
Log.d(TAG, "Compass sensor started");
} else {
Log.e(TAG, "Compass sensors not available");
}
}
public void stopListening() {
if (isListening) {
sensorManager.unregisterListener(this);
isListening = false;
compassListener = null;
// Сбрасываем фильтр
resetFilter();
Log.d(TAG, "Compass sensor stopped");
}
}
/**
* Сбрасывает фильтр
*/
private void resetFilter() {
bufferIndex = 0;
bufferFull = false;
for (int i = 0; i < FILTER_SIZE; i++) {
azimuthBuffer[i] = 0;
}
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.length);
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.length);
}
// Обновляем ориентацию
updateOrientationAngles();
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Можно добавить логику для обработки изменений точности
}
private void updateOrientationAngles() {
// Обновляем матрицу вращения
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading);
// Получаем углы ориентации
SensorManager.getOrientation(rotationMatrix, orientationAngles);
// Азимут (направление на север) в радианах, конвертируем в градусы
float azimuthInRadians = orientationAngles[0];
float azimuthInDegrees = (float) Math.toDegrees(azimuthInRadians);
// Нормализуем до диапазона 0-360
if (azimuthInDegrees < 0) {
azimuthInDegrees += 360;
}
// Применяем скользящий фильтр
float filteredAzimuth = applyLowPassFilter(azimuthInDegrees);
// Уведомляем слушателя
if (compassListener != null) {
compassListener.onCompassChanged(filteredAzimuth);
}
}
/**
* Применяет скользящий фильтр для сглаживания значений
*/
private float applyLowPassFilter(float newValue) {
// Добавляем новое значение в буфер
azimuthBuffer[bufferIndex] = newValue;
bufferIndex = (bufferIndex + 1) % FILTER_SIZE;
if (bufferIndex == 0) {
bufferFull = true;
}
// Вычисляем среднее значение
float sum = 0;
int count = bufferFull ? FILTER_SIZE : bufferIndex;
for (int i = 0; i < count; i++) {
sum += azimuthBuffer[i];
}
return sum / count;
}
public boolean isAvailable() {
return accelerometer != null && magnetometer != null;
}
public boolean isListening() {
return isListening;
}
}
@@ -0,0 +1,115 @@
package com.grigowashere.aismap.utils;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.Vessel;
/**
* Утилиты для геодезических расчетов
*/
public class GeoUtils {
private static final double EARTH_RADIUS = 6371000; // радиус Земли в метрах
/**
* Рассчитывает расстояние между двумя точками по формуле гаверсинуса
* @param lat1 широта первой точки в градусах
* @param lon1 долгота первой точки в градусах
* @param lat2 широта второй точки в градусах
* @param lon2 долгота второй точки в градусах
* @return расстояние в метрах
*/
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLat = Math.toRadians(lat2 - lat1);
double deltaLon = Math.toRadians(lon2 - lon1);
double a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) +
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS * c;
}
/**
* Рассчитывает расстояние от нашего судна до AIS судна
* @param ourVessel наше судно
* @param aisVessel AIS судно
* @return расстояние в метрах
*/
public static double calculateDistance(Vessel ourVessel, AISVessel aisVessel) {
return calculateDistance(
ourVessel.getLatitude(), ourVessel.getLongitude(),
aisVessel.getLatitude(), aisVessel.getLongitude()
);
}
/**
* Рассчитывает пеленг от первой точки ко второй
* @param lat1 широта первой точки в градусах
* @param lon1 долгота первой точки в градусах
* @param lat2 широта второй точки в градусах
* @param lon2 долгота второй точки в градусах
* @return пеленг в градусах (0-360)
*/
public static double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
double lat1Rad = Math.toRadians(lat1);
double lat2Rad = Math.toRadians(lat2);
double deltaLon = Math.toRadians(lon2 - lon1);
double y = Math.sin(deltaLon) * Math.cos(lat2Rad);
double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLon);
double bearing = Math.toDegrees(Math.atan2(y, x));
return (bearing + 360) % 360; // нормализуем к диапазону 0-360
}
/**
* Рассчитывает пеленг от нашего судна до AIS судна
* @param ourVessel наше судно
* @param aisVessel AIS судно
* @return пеленг в градусах (0-360)
*/
public static double calculateBearing(Vessel ourVessel, AISVessel aisVessel) {
return calculateBearing(
ourVessel.getLatitude(), ourVessel.getLongitude(),
aisVessel.getLatitude(), aisVessel.getLongitude()
);
}
/**
* Конвертирует навигационный статус в числовой код
* @param navigationalStatus строковый статус
* @return числовой код статуса
*/
public static int getNavigationStatusCode(String navigationalStatus) {
if (navigationalStatus == null) return -1;
switch (navigationalStatus.toLowerCase()) {
case "under way using engine":
case "under way":
return 0;
case "at anchor":
case "anchored":
return 1;
case "not under command":
return 2;
case "restricted manoeuvrability":
return 3;
case "constrained by her draught":
return 4;
case "moored":
return 5;
case "aground":
return 6;
case "engaged in fishing":
return 7;
case "under way sailing":
return 8;
default:
return -1;
}
}
}
@@ -0,0 +1,442 @@
package com.grigowashere.aismap.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
public abstract class BaseDockWidget extends FrameLayout {
private static final String TAG = "BaseDockWidget";
// Константы
protected static final int CIRCLE_SIZE_DP = 120;
protected static final int DEFAULT_DOCK_HEIGHT_DP = 80;
protected static final float MIN_SCALE = 0.5f;
protected static final float MAX_SCALE = 2.0f;
protected static final float SCALE_STEP = 0.1f;
// Состояние виджета
protected boolean isDocked = true; // По умолчанию в dock-режиме
protected boolean dockTop = true;
protected boolean isMorphing = false;
protected float morphProgress = 0.0f; // 0 = dock, 1 = circle
// Перетаскивание
protected boolean dragging = false;
protected float dX, dY;
protected Float targetDragX = null;
protected Float targetDragY = null;
// Изменение размера в dock режиме
protected boolean resizingDock = false;
protected float lastTouchY;
protected int dockHeightPx = 0;
// Масштабирование
protected float scaleFactor = 1.0f;
protected float initialDistance = 0;
protected float initialScale = 1.0f;
// Анимация
protected ValueAnimator morphAnimator;
// Интерфейс для уведомления об изменении размера
public interface OnDockResizeListener {
void onDockResize(int newHeight);
}
protected OnDockResizeListener dockResizeListener;
public BaseDockWidget(Context context) {
super(context);
init();
}
public BaseDockWidget(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setClickable(true);
setFocusable(true);
// Инициализируем в dock-режиме
post(() -> {
if (isDocked) {
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
setX(0);
setY(0);
ViewGroup.LayoutParams lp = getLayoutParams();
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
lp.height = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
dockHeightPx = 0; // Сбрасываем сохраненную высоту
setLayoutParams(lp);
}
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isMorphing) return true;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return handleTouchDown(event);
case MotionEvent.ACTION_MOVE:
return handleTouchMove(event);
case MotionEvent.ACTION_UP:
return handleTouchUp(event);
case MotionEvent.ACTION_POINTER_DOWN:
return handlePointerDown(event);
case MotionEvent.ACTION_POINTER_UP:
return handlePointerUp(event);
}
return super.onTouchEvent(event);
}
private boolean handleTouchDown(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (isDocked) {
// Проверяем зоны изменения размера в зависимости от позиции закрепления
if (dockTop) {
// Если закреплен сверху, зона изменения размера только снизу
if (y > getHeight() - dp(24)) {
resizingDock = true;
lastTouchY = event.getRawY();
return true;
}
} else {
// Если закреплен снизу, зона изменения размера только сверху
if (y < dp(24)) {
resizingDock = true;
lastTouchY = event.getRawY();
return true;
}
}
// Если нажали в центральной области dock-виджета, переводим в movable режим
// Вычисляем новую позицию, чтобы виджет был под пальцем
float newX = event.getRawX() - dp(CIRCLE_SIZE_DP) / 2;
float newY = event.getRawY() - dp(CIRCLE_SIZE_DP) / 2;
// Ограничиваем в пределах экрана
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
newX = Math.max(0, Math.min(newX, parent.getWidth() - dp(CIRCLE_SIZE_DP)));
newY = Math.max(0, Math.min(newY, parent.getHeight() - dp(CIRCLE_SIZE_DP)));
}
setDocked(false, dockTop, newX, newY);
}
// Обычное перетаскивание
dragging = true;
dX = getX() - event.getRawX();
dY = getY() - event.getRawY();
return true;
}
private boolean handleTouchMove(MotionEvent event) {
if (resizingDock) {
handleDockResize(event);
return true;
}
// Обработка масштабирования двумя пальцами
if (event.getPointerCount() == 2 && initialDistance > 0) {
float distance = getDistance(event);
float scale = distance / initialDistance;
scaleFactor = Math.max(MIN_SCALE, Math.min(MAX_SCALE, initialScale * scale));
requestLayout();
return true;
}
if (dragging) {
float newX = event.getRawX() + dX;
float newY = event.getRawY() + dY;
// Ограничиваем движение в пределах родителя
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
newX = Math.max(0, Math.min(newX, parent.getWidth() - getWidth()));
newY = Math.max(0, Math.min(newY, parent.getHeight() - getHeight()));
}
setX(newX);
setY(newY);
// Проверяем возможность докинга
checkDocking(event);
return true;
}
return false;
}
private boolean handleTouchUp(MotionEvent event) {
if (resizingDock) {
resizingDock = false;
return true;
}
if (dragging) {
dragging = false;
// Если виджет находится в зоне докинга, доким его
if (shouldDock(event)) {
performDocking(event);
}
return true;
}
return false;
}
private boolean handlePointerDown(MotionEvent event) {
if (event.getPointerCount() == 2) {
initialDistance = getDistance(event);
initialScale = scaleFactor;
}
return true;
}
private boolean handlePointerUp(MotionEvent event) {
if (event.getPointerCount() < 2) {
initialDistance = 0;
}
return true;
}
private void handleDockResize(MotionEvent event) {
float deltaY = event.getRawY() - lastTouchY;
lastTouchY = event.getRawY();
ViewGroup.LayoutParams lp = getLayoutParams();
int newHeight = lp.height;
// Направление изменения размера зависит от позиции закрепления
if (dockTop) {
// Если закреплен сверху, увеличиваем размер при движении вниз
newHeight += (int) deltaY;
} else {
// Если закреплен снизу, увеличиваем размер при движении вверх
newHeight -= (int) deltaY;
}
// Ограничиваем минимальную и максимальную высоту
int minHeight = (int) dp(40);
int maxHeight = ((ViewGroup) getParent()).getHeight() / 2;
newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
if (newHeight != lp.height) {
lp.height = newHeight;
dockHeightPx = newHeight;
setLayoutParams(lp);
// Если закреплен снизу, нужно также изменить позицию Y
if (!dockTop) {
float newY = ((ViewGroup) getParent()).getHeight() - newHeight;
setY(newY);
}
if (dockResizeListener != null) {
dockResizeListener.onDockResize(newHeight);
}
}
}
private void checkDocking(MotionEvent event) {
// Проверяем расстояние до краев экрана
float screenHeight = ((ViewGroup) getParent()).getHeight();
float y = event.getRawY();
float dockThreshold = dp(100);
if (y < dockThreshold) {
// Близко к верхнему краю
targetDragX = 0f;
targetDragY = 0f;
} else if (y > screenHeight - dockThreshold) {
// Близко к нижнему краю
targetDragX = 0f;
targetDragY = screenHeight - getHeight();
} else {
targetDragX = null;
targetDragY = null;
}
}
private boolean shouldDock(MotionEvent event) {
return targetDragX != null && targetDragY != null;
}
private void performDocking(MotionEvent event) {
float screenHeight = ((ViewGroup) getParent()).getHeight();
float y = event.getRawY();
boolean dockToTop = y < screenHeight / 2;
// При докинге всегда устанавливаем размер по умолчанию
dockHeightPx = 0; // Сбрасываем сохраненную высоту
setDocked(true, dockToTop, 0f, dockToTop ? 0f : screenHeight - dp(DEFAULT_DOCK_HEIGHT_DP));
}
private float getDistance(MotionEvent event) {
if (event.getPointerCount() < 2) return 0;
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (isDocked) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = dockHeightPx > 0 ? dockHeightPx : (int) dp(DEFAULT_DOCK_HEIGHT_DP);
setMeasuredDimension(width, height);
} else {
int size = (int)(dp(CIRCLE_SIZE_DP) * scaleFactor);
setMeasuredDimension(size, size);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Вызываем соответствующий метод отрисовки
if (isDocked) {
onDrawDock(canvas);
} else {
onDrawCircle(canvas);
}
}
public void setDocked(boolean docked, boolean top) {
setDocked(docked, top, getX(), getY());
}
public void setDocked(boolean docked, boolean top, float targetX, float targetY) {
if (this.isDocked == docked && this.dockTop == top && getX() == targetX && getY() == targetY) {
return;
}
this.dockTop = top;
if (morphAnimator != null && morphAnimator.isRunning()) {
morphAnimator.cancel();
}
float startMorph = morphProgress;
float endMorph = docked ? 0f : 1f;
int startW = getWidth();
int startH = getHeight();
ViewGroup parent = (ViewGroup) getParent();
int parentWidth = parent.getWidth();
int parentHeight = parent.getHeight();
int dockHeight = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
int circleSize = (int) dp(CIRCLE_SIZE_DP);
int endW = docked ? parentWidth : circleSize;
int endH = docked ? dockHeight : circleSize;
float startX = getX();
float startY = getY();
float endX = targetX;
float endY = targetY;
// Если доким в нижнюю часть, корректируем позицию Y
if (docked && !top) {
endY = parentHeight - dockHeight;
}
// Сохраняем финальные значения для использования в lambda и inner class
final float finalStartX = startX;
final float finalStartY = startY;
final float finalEndX = endX;
final float finalEndY = endY;
morphAnimator = ValueAnimator.ofFloat(0f, 1f);
morphAnimator.setDuration(350);
morphAnimator.addUpdateListener(anim -> {
float t = (float) anim.getAnimatedValue();
morphProgress = startMorph + (endMorph - startMorph) * t;
int w = (int) (startW + (endW - startW) * t);
int h = (int) (startH + (endH - startH) * t);
ViewGroup.LayoutParams lp = getLayoutParams();
lp.width = w;
lp.height = h;
setLayoutParams(lp);
setX(finalStartX + (finalEndX - finalStartX) * t);
setY(finalStartY + (finalEndY - finalStartY) * t);
postInvalidateOnAnimation();
});
morphAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
ViewGroup.LayoutParams lp = getLayoutParams();
lp.width = endW;
lp.height = endH;
setLayoutParams(lp);
setX(finalEndX);
setY(finalEndY);
morphProgress = endMorph;
postInvalidateOnAnimation();
isMorphing = false;
}
});
morphAnimator.start();
this.isDocked = docked;
isMorphing = true;
}
public boolean isDocked() {
return isDocked;
}
public boolean isDockTop() {
return dockTop;
}
protected boolean isMorphing() {
return isMorphing;
}
public void setOnDockResizeListener(OnDockResizeListener listener) {
this.dockResizeListener = listener;
}
protected float dp(float dp) {
return dp * getResources().getDisplayMetrics().density;
}
// Абстрактные методы для переопределения в наследниках
protected abstract void onDrawDock(Canvas canvas);
protected abstract void onDrawCircle(Canvas canvas);
}
@@ -0,0 +1,334 @@
package com.grigowashere.aismap.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.utils.GeoUtils;
import java.util.ArrayList;
import java.util.List;
public class CompassView extends BaseDockWidget {
private static final String TAG = "CompassView";
private float targetAzimuth = 0;
private float currentAzimuth = 0;
private float magneticCompass = 0; // магнитный компас
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint vesselPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path vesselPath = new Path();
private final String[] directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
private float centerX;
private float centerY;
private static final float SMOOTHING_FACTOR = 0.15f;
private List<AISVessel> nearbyVessels = new ArrayList<>();
private Vessel ourVessel; // наше судно для расчета расстояний
private static final float MAX_DISPLAY_DISTANCE = 10000; // 10 км
private static final float MIN_VESSEL_SIZE = 10; // минимальный размер значка
private static final float MAX_VESSEL_SIZE = 30; // максимальный размер значка
public CompassView(Context context) {
super(context);
init();
}
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paint.setColor(Color.WHITE);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(36f);
vesselPaint.setStyle(Paint.Style.FILL);
vesselPaint.setAntiAlias(true);
// Устанавливаем фон для видимости
setBackgroundColor(Color.argb(200, 0, 0, 0));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2f;
centerY = h / 2f;
}
private float getShortestRotation(float start, float end) {
float diff = end - start;
while (diff > 180) diff -= 360;
while (diff < -180) diff += 360;
return diff;
}
// Прямая шкала (dock-режим)
@Override
protected void onDrawDock(Canvas canvas) {
Log.d(TAG, "onDrawDock called, width=" + getWidth() + ", height=" + getHeight());
float w = getWidth();
float h = getHeight();
if (w <= 0 || h <= 0) {
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
return;
}
// Простой фон для начала
paint.setColor(Color.argb(200, 0, 0, 0));
canvas.drawRect(0, 0, w, h, paint);
// Масштабируем размеры в зависимости от высоты виджета
float baseHeight = dp(80); // базовая высота
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
// Простой текст для проверки
paint.setColor(Color.WHITE);
paint.setTextSize(24 * scaleFactor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("КОМПАС", w/2, h/2, paint);
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
// Плавное обновление азимута
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
if (Math.abs(diff) > 0.1f) {
currentAzimuth += diff * SMOOTHING_FACTOR;
if (currentAzimuth > 360) currentAzimuth -= 360;
if (currentAzimuth < 0) currentAzimuth += 360;
postInvalidateOnAnimation();
}
// Рисуем простую шкалу
float centerX = w / 2f;
float centerY = h / 2f;
float visibleDegrees = 120;
// Рисуем деления шкалы
for (int degree = 0; degree < 360; degree += 15) {
// Вычисляем относительное положение деления
float relativeDegree = (degree - currentAzimuth + 360) % 360;
if (relativeDegree > 180) relativeDegree -= 360;
// Рисуем только видимые деления
if (Math.abs(relativeDegree) <= visibleDegrees / 2) {
float x = centerX + (relativeDegree / (visibleDegrees / 2)) * (w / 2);
float lineHeight = (degree % 30 == 0) ? 20 * scaleFactor : 10 * scaleFactor;
canvas.drawLine(x, centerY - lineHeight, x, centerY + lineHeight, paint);
if (degree % 30 == 0) {
String degreeText = String.valueOf(degree);
paint.setTextSize(16 * scaleFactor);
canvas.drawText(degreeText, x, centerY - 30 * scaleFactor, paint);
}
if (degree % 45 == 0) {
int directionIndex = (degree / 45) % 8;
if (directionIndex < directions.length) {
paint.setTextSize(18 * scaleFactor);
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
}
}
}
}
// Рисуем суда
for (AISVessel vessel : nearbyVessels) {
float relativeBearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
if (relativeBearing > 180) relativeBearing -= 360;
if (Math.abs(relativeBearing) <= visibleDegrees / 2) {
float x = centerX + (relativeBearing / (visibleDegrees / 2)) * (w / 2);
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
float size = calculateVesselSize((float) distance) * scaleFactor;
vesselPaint.setColor(getVesselColor(vessel));
drawVesselTriangle(canvas, x, centerY, size, (float) (vessel.getCourse() - currentAzimuth));
}
}
// Центральная линия (направление вперёд)
paint.setColor(Color.RED);
paint.setStrokeWidth(3 * scaleFactor);
canvas.drawLine(centerX, centerY - h/2, centerX, centerY + h/2, paint);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(1);
// Выделяем зону resize в зависимости от позиции закрепления
if (isDocked) {
Paint resizePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
resizePaint.setColor(Color.argb(120, 255, 255, 255));
resizePaint.setStyle(Paint.Style.STROKE);
resizePaint.setStrokeWidth(2);
paint.setTextSize(12);
paint.setColor(Color.WHITE);
if (isDockTop()) {
// Если закреплен сверху, показываем зону resize снизу
canvas.drawRect(0, h - dp(24), w, h, resizePaint);
canvas.drawText("", w/2, h - dp(12), paint);
} else {
// Если закреплен снизу, показываем зону resize сверху
canvas.drawRect(0, 0, w, dp(24), resizePaint);
canvas.drawText("", w/2, dp(12), paint);
}
}
}
// Круглый компас (draggable-режим)
@Override
protected void onDrawCircle(Canvas canvas) {
Log.d(TAG, "onDrawCircle called, width=" + getWidth() + ", height=" + getHeight());
float w = getWidth();
float h = getHeight();
if (w <= 0 || h <= 0) {
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
return;
}
float cx = w / 2f;
float cy = h / 2f;
float radius = Math.min(w, h) / 2f * 0.9f;
// Масштабируем размеры в зависимости от размера виджета
float baseSize = dp(120); // базовая высота
float scaleFactor = Math.max(0.8f, Math.min(2.0f, Math.min(w, h) / baseSize));
// Фон
paint.setColor(Color.argb(200, 0, 0, 0));
canvas.drawCircle(cx, cy, radius, paint);
paint.setColor(Color.WHITE);
// Плавное обновление азимута
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
if (Math.abs(diff) > 0.1f) {
currentAzimuth += diff * SMOOTHING_FACTOR;
if (currentAzimuth > 360) currentAzimuth -= 360;
if (currentAzimuth < 0) currentAzimuth += 360;
postInvalidateOnAnimation();
}
// Деления и метки по кругу
for (int degree = 0; degree < 360; degree += 30) {
float angle = (float) Math.toRadians(degree - currentAzimuth);
float x1 = cx + (float) Math.sin(angle) * (radius * 0.85f);
float y1 = cy - (float) Math.cos(angle) * (radius * 0.85f);
float x2 = cx + (float) Math.sin(angle) * radius;
float y2 = cy - (float) Math.cos(angle) * radius;
paint.setStrokeWidth(2 * scaleFactor);
canvas.drawLine(x1, y1, x2, y2, paint);
if (degree % 90 == 0) {
int directionIndex = (degree / 90) % 4;
String[] mainDirections = {"N", "E", "S", "W"};
float dx = cx + (float) Math.sin(angle) * (radius - 25 * scaleFactor);
float dy = cy - (float) Math.cos(angle) * (radius - 25 * scaleFactor);
paint.setTextSize(16 * scaleFactor);
canvas.drawText(mainDirections[directionIndex], dx, dy, paint);
}
}
// Рисуем суда по кругу
for (AISVessel vessel : nearbyVessels) {
float bearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
float angle = (float) Math.toRadians(bearing);
float vesselRadius = radius * 0.6f;
float vx = cx + (float) Math.sin(angle) * vesselRadius;
float vy = cy - (float) Math.cos(angle) * vesselRadius;
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
float size = calculateVesselSize((float) distance) * scaleFactor;
vesselPaint.setColor(getVesselColor(vessel));
drawVesselTriangle(canvas, vx, vy, size, (float) (vessel.getCourse() - currentAzimuth));
}
// Центральная линия (направление вперёд)
paint.setColor(Color.RED);
paint.setStrokeWidth(3 * scaleFactor);
canvas.drawLine(cx, cy, cx, cy - radius, paint);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(1);
// Текст азимута в центре
paint.setTextSize(14 * scaleFactor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText((int)currentAzimuth + "°", cx, cy + 5 * scaleFactor, paint);
}
private float calculateVesselSize(float distance) {
if (distance > MAX_DISPLAY_DISTANCE) return MIN_VESSEL_SIZE;
// Линейная интерполяция размера от расстояния
float ratio = 1 - Math.min(distance / MAX_DISPLAY_DISTANCE, 1);
return MIN_VESSEL_SIZE + (MAX_VESSEL_SIZE - MIN_VESSEL_SIZE) * ratio;
}
private int getVesselColor(AISVessel vessel) {
// Можно настроить цвета в зависимости от параметров судна
// Используем navigation status из AIS данных
int navStatus = GeoUtils.getNavigationStatusCode(vessel.getNavigationalStatus());
switch (navStatus) {
case 0: // Under way using engine
return Color.GREEN;
case 1: // At anchor
return Color.YELLOW;
case 5: // Moored
return Color.BLUE;
default:
return Color.WHITE;
}
}
private void drawVesselTriangle(Canvas canvas, float x, float y, float size, float rotation) {
vesselPath.reset();
// Создаем треугольник
float halfSize = size / 2;
vesselPath.moveTo(x, y - halfSize); // вершина
vesselPath.lineTo(x - halfSize, y + halfSize); // левый нижний угол
vesselPath.lineTo(x + halfSize, y + halfSize); // правый нижний угол
vesselPath.close();
// Поворачиваем треугольник
canvas.save();
canvas.rotate(rotation, x, y);
canvas.drawPath(vesselPath, vesselPaint);
canvas.restore();
}
public void setAzimuth(float azimuth) {
this.targetAzimuth = azimuth;
invalidate();
}
public void setMagneticCompass(float magneticCompass) {
this.magneticCompass = magneticCompass;
invalidate();
}
public void updateNearbyVessels(List<AISVessel> vessels) {
this.nearbyVessels = vessels;
invalidate();
}
public void setOurVessel(Vessel ourVessel) {
this.ourVessel = ourVessel;
invalidate();
}
}
+13
View File
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,3l-13,26l13,-5l13,5z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
+101
View File
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<!-- Карта -->
<com.yandex.mapkit.mapview.MapView
android:id="@+id/map_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- Панель управления -->
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_margin="16dp"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="8dp"
android:elevation="4dp">
<Button
android:id="@+id/btn_center_vessel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Центр на судне"
android:textSize="12sp"
android:minWidth="100dp" />
<Button
android:id="@+id/btn_test_compass"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Тест компаса"
android:textSize="12sp"
android:minWidth="100dp"
android:layout_marginTop="8dp" />
</LinearLayout>
<!-- Компас -->
<com.grigowashere.aismap.view.CompassView
android:id="@+id/compass_view"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_alignParentTop="true"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="0dp" />
<!-- Простая информационная панель
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentStart="true"
android:layout_margin="16dp"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="12dp"
android:elevation="4dp">
<TextView
android:id="@+id/tv_status"
android:layout_width="139dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Статус: Инициализация..."
android:textColor="@android:color/black"
android:textSize="12sp" />
<TextView
android:id="@+id/tv_ais_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🚢 AIS суда: 0"
android:textSize="12sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btn_show_vessel_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📋 Информация о судне"
android:textSize="11sp"
android:minHeight="36dp"
android:background="@android:color/holo_blue_light"
android:textColor="@android:color/white" />
</LinearLayout> -->
</RelativeLayout>
@@ -0,0 +1,256 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="16dp">
<!-- Заголовок с кнопкой закрытия -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:id="@+id/bottom_sheet_ais_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="🚢 AIS СУДНО"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black" />
<ImageButton
android:id="@+id/btn_close_ais_bottom_sheet"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Закрыть" />
</LinearLayout>
<!-- Основная информация -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="400dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- MMSI -->
<TextView
android:id="@+id/bottom_sheet_ais_mmsi"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🆔 MMSI: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Название судна -->
<TextView
android:id="@+id/bottom_sheet_ais_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📛 Название: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Позывной -->
<TextView
android:id="@+id/bottom_sheet_ais_callsign"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📻 Позывной: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- IMO -->
<TextView
android:id="@+id/bottom_sheet_ais_imo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🏷️ IMO: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Тип судна -->
<TextView
android:id="@+id/bottom_sheet_ais_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🚢 Тип: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Координаты -->
<TextView
android:id="@+id/bottom_sheet_ais_position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📍 Координаты: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Курс -->
<TextView
android:id="@+id/bottom_sheet_ais_course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🧭 Курс: --°"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Скорость -->
<TextView
android:id="@+id/bottom_sheet_ais_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚡ Скорость: -- узлов"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Размеры -->
<TextView
android:id="@+id/bottom_sheet_ais_dimensions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📏 Размеры: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Осадка -->
<TextView
android:id="@+id/bottom_sheet_ais_draft"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🌊 Осадка: -- м"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Пункт назначения -->
<TextView
android:id="@+id/bottom_sheet_ais_destination"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🎯 Назначение: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- ETA -->
<TextView
android:id="@+id/bottom_sheet_ais_eta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⏰ ETA: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Навигационный статус -->
<TextView
android:id="@+id/bottom_sheet_ais_nav_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🚦 Статус: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Класс судна -->
<TextView
android:id="@+id/bottom_sheet_ais_class"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📋 Класс: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Сила сигнала -->
<TextView
android:id="@+id/bottom_sheet_ais_signal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📶 Сигнал: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Последнее обновление -->
<TextView
android:id="@+id/bottom_sheet_ais_last_update"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🕐 Обновлено: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Время назад -->
<TextView
android:id="@+id/bottom_sheet_ais_time_ago"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⏱️ Время назад: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -0,0 +1,183 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/white"
android:orientation="vertical"
android:padding="16dp">
<!-- Заголовок с кнопкой закрытия -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="🚢 НАШЕ СУДНО"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black" />
<ImageButton
android:id="@+id/btn_close_bottom_sheet"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:contentDescription="Закрыть" />
</LinearLayout>
<!-- Основная информация -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxHeight="400dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Статус -->
<TextView
android:id="@+id/bottom_sheet_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Статус: Инициализация..."
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Координаты -->
<TextView
android:id="@+id/bottom_sheet_position"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📍 Координаты: Не определены"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Курс -->
<TextView
android:id="@+id/bottom_sheet_course"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🧭 Курс: --°"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Скорость -->
<TextView
android:id="@+id/bottom_sheet_speed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚡ Скорость: -- узлов"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Высота -->
<TextView
android:id="@+id/bottom_sheet_altitude"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🏔️ Высота: -- м"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Точность -->
<TextView
android:id="@+id/bottom_sheet_accuracy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🎯 Точность: -- м"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Качество GPS -->
<TextView
android:id="@+id/bottom_sheet_gps_quality"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📊 Качество GPS: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Спутники -->
<TextView
android:id="@+id/bottom_sheet_satellites"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Спутники: --/--"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- DOP значения -->
<TextView
android:id="@+id/bottom_sheet_dop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📈 DOP: PDOP=-- HDOP=-- VDOP=--"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Время фикса -->
<TextView
android:id="@+id/bottom_sheet_fix_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🕐 Время фикса: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
<!-- Качество фикса -->
<TextView
android:id="@+id/bottom_sheet_fix_quality"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🔒 Качество фикса: --"
android:textSize="14sp"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp"
android:background="@android:color/transparent"
android:padding="8dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_gps"
android:title="GPS"
android:icon="@android:drawable/ic_menu_mylocation"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_udp"
android:title="UDP"
android:icon="@android:drawable/ic_menu_send"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_clear_ais"
android:title="Очистить AIS"
android:icon="@android:drawable/ic_menu_delete"
app:showAsAction="ifRoom" />
</menu>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 MiB

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

After

Width:  |  Height:  |  Size: 419 B

+7
View File
@@ -0,0 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AISMap" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your dark theme here. -->
<!-- <item name="colorPrimary">@color/my_dark_primary</item> -->
</style>
</resources>
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">AISMap</string>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.AISMap" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.AISMap" parent="Base.Theme.AISMap" />
</resources>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older than API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.grigowashere.aismap;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() {
assertEquals(4, 2 + 2);
}
}
+4
View File
@@ -0,0 +1,4 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
}
+21
View File
@@ -0,0 +1,21 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
+22
View File
@@ -0,0 +1,22 @@
[versions]
agp = "8.9.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.1"
material = "1.12.0"
activity = "1.10.1"
constraintlayout = "2.2.1"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
Binary file not shown.
+6
View File
@@ -0,0 +1,6 @@
#Tue Aug 26 09:19:58 MSK 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
Vendored
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+23
View File
@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "AISMap"
include ':app'
+72
View File
@@ -0,0 +1,72 @@
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class TestAISDecoding {
public static void main(String[] args) {
TestAISDecoding test = new TestAISDecoding();
// Тестируем AIS сообщение типа 1
String ais1 = "17RdG7V04d039I5wwj2kh30d050l";
System.out.println("=== Тест AIS типа 1 ===");
test.testDecodeAISField(ais1, 8, 30, "MMSI");
test.testDecodeAISField(ais1, 38, 4, "Navigation Status");
test.testDecodeAISField(ais1, 50, 10, "Speed");
test.testDecodeAISField(ais1, 61, 28, "Longitude");
test.testDecodeAISField(ais1, 89, 27, "Latitude");
test.testDecodeAISField(ais1, 116, 12, "Course");
// Тестируем AIS сообщение типа 5 (собранное из фрагментов)
String ais5 = "57RdG7T1M>wh4U?62204U?62222222222222220R:0D5?1Uf4<Q1APEC588888888888882";
System.out.println("\n=== Тест AIS типа 5 ===");
test.testDecodeAISField(ais5, 8, 30, "MMSI");
test.testDecodeAISField(ais5, 38, 2, "AIS Version");
test.testDecodeAISField(ais5, 40, 30, "IMO");
test.testDecodeAISField(ais5, 70, 42, "Call Sign");
test.testDecodeAISField(ais5, 112, 120, "Vessel Name");
test.testDecodeAISField(ais5, 232, 8, "Ship Type");
test.testDecodeAISField(ais5, 256, 10, "Length");
test.testDecodeAISField(ais5, 266, 10, "Width");
test.testDecodeAISField(ais5, 276, 8, "Draft");
}
private void testDecodeAISField(String payload, int startBit, int length, String fieldName) {
String bits = decodeAISField(payload, startBit, length);
System.out.printf("%s (биты %d-%d): %s (длина: %d)%n",
fieldName, startBit, startBit + length - 1, bits, bits.length());
if (length <= 30) {
try {
int value = Integer.parseInt(bits, 2);
System.out.printf(" Десятичное значение: %d%n", value);
} catch (NumberFormatException e) {
System.out.printf(" Ошибка парсинга: %s%n", e.getMessage());
}
}
}
private String decodeAISField(String payload, int startBit, int length) {
StringBuilder result = new StringBuilder();
// Преобразуем каждый символ payload в 6-битное значение
for (int i = 0; i < payload.length(); i++) {
char c = payload.charAt(i);
int value = c - 48; // AIS использует ASCII 48-119 для значений 0-71
if (value < 0) value += 64;
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 {
System.out.printf(" ВНИМАНИЕ: AIS поле выходит за границы: startBit=%d, length=%d, payloadLength=%d, binaryLength=%d%n",
startBit, length, payload.length(), fullBinary.length());
return fullBinary.substring(startBit, Math.min(startBit + length, fullBinary.length()));
}
}
}