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()));
|
||||
}
|
||||
}
|
||||
}
|
||||