Initial commit: AIS Map Android application
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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. Добавить логирование и обработку ошибок
|
||||||
|
|
||||||
|
### Кастомные поля
|
||||||
|
- Поддержка региональных расширений
|
||||||
|
- Обработка производитель-специфичных данных
|
||||||
|
- Гибкая система метаданных
|
||||||
@@ -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 системы и богатство морских навигационных данных.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
- [ ] Сохранение истории маршрутов
|
||||||
|
- [ ] Экспорт данных
|
||||||
|
- [ ] Настройки отображения
|
||||||
|
- [ ] Многоязычная поддержка
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Проект разработан для образовательных целей.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/build
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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'
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 7.0 MiB |
|
After Width: | Height: | Size: 7.0 MiB |
|
After Width: | Height: | Size: 7.0 MiB |
@@ -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 |
@@ -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>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="black">#FF000000</color>
|
||||||
|
<color name="white">#FFFFFFFF</color>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">AISMap</string>
|
||||||
|
</resources>
|
||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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" }
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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" "$@"
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
@@ -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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||