Merge pull request 'new-approach' (#4) from new-approach into master
Reviewed-on: Grigo/AISMap#4
@@ -1,5 +1,6 @@
|
||||
*.iml
|
||||
.gradle
|
||||
.idea
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
@@ -14,3 +15,43 @@
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.aab
|
||||
*.apk
|
||||
output-metadata.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Android Studio / IntelliJ IDEA
|
||||
*.iws
|
||||
.idea/libraries
|
||||
.idea/tasks.xml
|
||||
.idea/vcs.xml
|
||||
.idea/workspace.xml
|
||||
@@ -4,6 +4,14 @@
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2025-09-23T13:53:32.308312900Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=bc722e5b" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="T:/sources/repository" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,74 @@
|
||||
# Резюме архитектурных изменений для устранения зависаний
|
||||
|
||||
## **🎯 Проблемы найдены и исправлены:**
|
||||
|
||||
### **1. Утечки Handler объектов** ✅
|
||||
**Проблема**: Каждый раз создавался `new Handler().post()` для UI операций
|
||||
**Решение**:
|
||||
- Добавлен единый `uiHandler` в AppController
|
||||
- Все прямые `mapInterface` вызовы заменены на `uiDataNotifier`
|
||||
- Централизованная очистка Handler'ов в `cleanup()`
|
||||
|
||||
### **2. Отсутствие throttling UI операций** ✅
|
||||
**Проблема**: UI операции происходили хаотично без ограничений
|
||||
**Решение**:
|
||||
- Создан `UIRenderingCoordinator` с централизованным throttling
|
||||
- Vessel updates: 500мс throttling
|
||||
- AIS updates: 1сек throttling
|
||||
- Path updates: 2сек throttling
|
||||
|
||||
### **3. Прямые UI вызовы из контроллеров** ✅
|
||||
**Проблема**: `mapInterface.updateOwnVesselPosition()` вызывался напрямую из AppController
|
||||
**Решение**:
|
||||
- Создан интерфейс `UIDataChangeNotifier` для связи контроллеров с UI
|
||||
- AppController теперь уведомляет UI через `uiDataNotifier.onVesselPositionChanged()`
|
||||
- Полное разделение логики и UI представления
|
||||
|
||||
### **4. Архитектурное разделение ответственности** ✅
|
||||
**Было**: Контроллеры знали о UI деталях и делали прямые вызовы карты
|
||||
**Стало**:
|
||||
- **Контроллеры**: только модель данных, парсинг, вычисления
|
||||
- **UI Coordinator**: централизованная очередь UI операций с throttling
|
||||
- **MainActivity**: только Android жизненный цикл, не UI логика
|
||||
|
||||
## **📊 Математика улучшений:**
|
||||
|
||||
### **Было**:
|
||||
- ~50+ Handler'ов создаваемых ежеминутно
|
||||
- Хаотичные UI обновления каждые 100-1000мс без throttling
|
||||
- Прямые блокирующие операции в UI потоке
|
||||
|
||||
### **Стало**:
|
||||
- 1 переиспользуемый Handler в AppController
|
||||
- Централизованный throttling через UIRenderingCoordinator
|
||||
- Все UI операции батчинговые и предсказуемые
|
||||
|
||||
## **🔄 Новая архитектура потоков:**
|
||||
|
||||
```
|
||||
Background: GPS/NMEA/UDP → AppController → uiDataNotifier → UIRenderingCoordinator
|
||||
↓ ↓ ↓ ↓
|
||||
Parse Data → Update Model → Request UI → Throttled Rendering
|
||||
```
|
||||
|
||||
### **Throttling потоки:**
|
||||
- Vessel position: 500мс
|
||||
- AIS vessels: 1000мс
|
||||
- Path updates: 2000мс
|
||||
- All через единую очередь UIRenderingCoordinator
|
||||
|
||||
## **🚀 Ожидаемый результат:**
|
||||
|
||||
✅ **Полное устранение зависаний UI** через 30+ минуты работы
|
||||
✅ **Предсказуемая производительность** - контроллеры работают в фоне
|
||||
✅ **Стабильная работа карты** - нет перегрузки UI потока
|
||||
✅ **Масштабируемость** - легко добавить новые контроллеры
|
||||
✅ **Тестируемость** - контроллеры независимы от UI
|
||||
|
||||
## **🔧 Следующие шаги для полного решения:**
|
||||
|
||||
1. ✅ Реализована новая архитектура с UIRenderingCoordinator
|
||||
2. ✅ Заменены все прямые UI вызовы в AppController
|
||||
3. ⏳ **Протестировать** новую архитектуру на протяженной работе
|
||||
|
||||
**Главное**: Заменена архитектура от хаотичных UI вызовов к **централизованному throttling** через единую точку. Это должно полностью решить проблему зависаний!
|
||||
@@ -0,0 +1,94 @@
|
||||
# РЕАЛЬНОЕ исправление зависаний карты и кнопок
|
||||
|
||||
## НАЙДЕНА ИСТИННАЯ ПРИЧИНА!
|
||||
|
||||
**Главная проблема**: В `MapLibreMapImpl.updateOwnVesselPosition()` вызывалось **ТРИ отдельных** `uiHandler.post()` операции:
|
||||
|
||||
```java
|
||||
// СТАРЫЙ КОД - ПРОБЛЕМНЫЙ:
|
||||
uiHandler.post(() -> updateOwnVesselPathSource("own_vessel", pathCoords)); // 1
|
||||
uiHandler.post(() -> updateOwnVesselPredictionSource("own_vessel", vessel)); // 2
|
||||
uiHandler.post(() -> refreshGeoJson()); // 3
|
||||
```
|
||||
|
||||
## Проблемы создававшие блокировки:
|
||||
|
||||
### 1. **Множественные UI операции**
|
||||
Каждое обновление GPS/NMEA вызывало **4 раза** `updateOwnVesselPosition` из AppController:
|
||||
- Из GPS `onLocationUpdated` (2 раза)
|
||||
- Из NMEA `onVesselUpdated` (2 раза)
|
||||
|
||||
### 2. **Тяжелые операции в UI потоке**:
|
||||
- `refreshGeoJson()` - пересоздание всей GeoJSON каждый раз
|
||||
- `updateOwnVesselPathSource()` - обновление источника с множеством координат
|
||||
- `updateOwnVesselPredictionSource()` - расчет прогноза
|
||||
|
||||
### 3. **reffreshGeoJson() проблематичен**:
|
||||
```java
|
||||
private void refreshGeoJson() {
|
||||
JSONObject fc = new JSONObject();
|
||||
fc.put("type", "FeatureCollection");
|
||||
JSONArray features = new JSONArray();
|
||||
for (JSONObject f : idToFeature.values()) { // Итерация по ВСЕМ судам
|
||||
features.put(f); // Объект преобразуется в JSON
|
||||
}
|
||||
fc.put("features", features);
|
||||
source.setGeoJson(fc.toString()); // Создание большой строки!
|
||||
}
|
||||
```
|
||||
|
||||
## ВНЕСЕННЫЕ ИСПРАВЛЕНИЯ:
|
||||
|
||||
### 1. **Throttling система в MapLibreMapImpl**:
|
||||
```java
|
||||
// Новые переменные для throttling
|
||||
private final android.os.Handler mapUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
private Runnable mapUpdateRunnable;
|
||||
private boolean mapUpdatePending = false;
|
||||
private static final long MAP_UPDATE_DELAY = 500; // 500ms throttling
|
||||
```
|
||||
|
||||
### 2. **Переработанный updateOwnVesselPosition**:
|
||||
```java
|
||||
@Override
|
||||
public void updateOwnVesselPosition(Vessel vessel) {
|
||||
// Данные обновляются СРАЗУ (не блокирующее)
|
||||
JSONObject feature = buildFeature(...);
|
||||
idToFeature.put("own_vessel", feature);
|
||||
|
||||
// Throttled обновление карты
|
||||
updateMapThrottled(vessel);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Батчевое обновление карты**:
|
||||
```java
|
||||
private void updateMapBatched(Vessel vessel) {
|
||||
uiHandler.post(() -> {
|
||||
// ВСЕ операции в ОДНОМ UI вызове:
|
||||
updateOwnVesselPathSource("own_vessel", pathCoords);
|
||||
updateOwnVesselPredictionSource("own_vessel", vessel);
|
||||
refreshGeoJson(); // Только один раз!
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Убрали дублированные вызовы в AppController**:
|
||||
- Удалили **2 избыточных** вызова `updateOwnVesselPosition`
|
||||
- Теперь остается только **1 throttled** вызов вместо **4 обычных**
|
||||
|
||||
## Результат:
|
||||
|
||||
- ✅ **Throttling**: вместо постоянных обновлений - 1 раз в 500мс
|
||||
- ✅ **Батчинг**: вместо 3 отдельных UI вызовов - 1 объединенный
|
||||
- ✅ **Дедупликация**: вместо 4 вызовов из AppController - 1 throttled
|
||||
- ✅ **Защита от зависания**: cleanup handler'ов в cleanup()
|
||||
|
||||
## Ожидаемый эффект:
|
||||
|
||||
1. **Карта и кнопки перестанут зависать**
|
||||
2. **Доквиджеты продолжат работать** (они не затрагивались)
|
||||
3. **Фоновые процессы не пострадают**
|
||||
4. **Обновления карты станут плавными** вместо лагающих
|
||||
|
||||
**Ключевая диагностика**: Смотрите в логах `"Карта обновлена батчом"` - это означает что throttling работает правильно.
|
||||
@@ -0,0 +1,77 @@
|
||||
# Новая архитектура приложения
|
||||
|
||||
## Проблемы текущей архитектуры:
|
||||
|
||||
1. **AppController смешивает логику и UI**:
|
||||
- `mapInterface.updateOwnVesselPosition()` вызывается напрямую
|
||||
- `uiHandler.post(() -> mapInterface.addAISVesselMarker())`
|
||||
- Контроллеры знают о UI деталях
|
||||
|
||||
2. **Нет единого поток UI операций**:
|
||||
- Каждый контроллер делает свои UI вызовы
|
||||
- Нет централизованного throttling для карты
|
||||
- Перегрузка UI потока
|
||||
|
||||
## Новая архитектура:
|
||||
|
||||
### 1. **Контроллеры (Background Threads)**:
|
||||
- Только **обновляют модель данных** (ownVessel, aisVessels)
|
||||
- Только **вычисления** (paths, predictions, compass)
|
||||
- Только **парсинг данных** (NMEA, GPS, UDP)
|
||||
- **Не знают** о UI карте
|
||||
|
||||
### 2. **UI Coordinator (Main Thread)**:
|
||||
- **Единая точка** всех UI операций
|
||||
- **Централизованный throttling** (500мс, 1сек)
|
||||
- **Батчинг** операций карты
|
||||
- **Координация** между контроллерами и UI
|
||||
|
||||
### 3. **Data Flow**:
|
||||
|
||||
```
|
||||
Background Threads → Model Updates → UI Coordinator → Throttled Map Updates
|
||||
↓ ↓ ↓ ↓
|
||||
NMEA/GPS ownVessel Batched UI MapLibre
|
||||
Parsing AIS Data Operations Rendering
|
||||
```
|
||||
|
||||
## План реализации:
|
||||
|
||||
### Этап 1: Создать UI Coordinator
|
||||
```java
|
||||
public class UIRenderingCoordinator {
|
||||
private MapInterface mapInterface;
|
||||
private Handler uiHandler;
|
||||
private Runnable batchedUpdateRunnable;
|
||||
private Set<String> pendingVesselUpdates;
|
||||
private Map<String, AISVessel> pendingAISUpdates;
|
||||
|
||||
void requestVesselUpdate(Vessel vessel) { /* add to pending */ }
|
||||
void requestAISUpdate(AISVessel vessel) { /* add to pending */ }
|
||||
void executeBatchUpdate() { /* throttled map rendering */ }
|
||||
}
|
||||
```
|
||||
|
||||
### Этап 2: Рефакторить AppController
|
||||
- Убрать все `mapInterface.*` вызовы
|
||||
- Только обновлять `ownVessel` данные
|
||||
- Уведомлять `UIRenderingCoordinator` через интерфейс
|
||||
- Никаких `uiHandler.post()` в контроллерах
|
||||
|
||||
### Этап 3: Установить throttling
|
||||
- Все UI операции → UI Coordinator
|
||||
- Throttling 500мс для критичных операций
|
||||
- Throttling 1сек для некритичных (paths, compass)
|
||||
|
||||
### Этап 4: Батчинг операций
|
||||
- Собирать все изменения за период throttling
|
||||
- Одним вызовом обновить всю карту
|
||||
- Минимизировать количество `mapInterface` вызовов
|
||||
|
||||
## Ожидаемый результат:
|
||||
|
||||
✅ **Стабильная производительность** - контроллеры работают в фоне
|
||||
✅ **Предсказуемые зависания** - все UI операции через единую точку
|
||||
✅ **Масштабируемость** - легко добавить новые контроллеры
|
||||
✅ **Тестируемость** - контроллеры не зависят от UI
|
||||
✅ **Производительность** - минимум UI операций, максимум батчинга
|
||||
@@ -0,0 +1,116 @@
|
||||
# Анализ и исправление зависаний UI в MainActivity
|
||||
|
||||
## Выявленные проблемы:
|
||||
|
||||
### 1. **Основная причина зависания**: `updateControlPanelPosition()`
|
||||
- Функция вызывается **слишком часто** (7+ мест вызова)
|
||||
- Выполняет **дорогие операции в главном потоке**:
|
||||
- Множественные `getHeight()` вызывают **layout pass**
|
||||
- `setLayoutParams()` - одна из **самых дорогих операций** в Android UI
|
||||
- Множество логирования в главном потоке
|
||||
- Вызывается каждые несколько секунд из-за автоматических обновлений
|
||||
|
||||
### 2. **Цепочка блокировок**:
|
||||
```
|
||||
coordinatesWidget.updateVessel()
|
||||
→ invalidate()
|
||||
→ onDraw()
|
||||
→ getHeight()
|
||||
→ onDockResize callback
|
||||
→ updateControlPanelPosition()
|
||||
→ setLayoutParams() ← BLOCKING!
|
||||
```
|
||||
|
||||
### 3. **Множественные UI обновления**:
|
||||
- `messageAgeRunnable` - каждую секунду
|
||||
- `bottomSheetUpdateRunnable` - каждую секунду
|
||||
- `timeUpdateRunnable` - каждую секунду
|
||||
- Все в главном UI потоке без throttling
|
||||
|
||||
## Внесенные исправления:
|
||||
|
||||
### 1. **Throttling для `updateControlPanelPosition`**:
|
||||
```java
|
||||
// Добавлены переменные для throttling
|
||||
private android.os.Handler controlPanelUpdateHandler;
|
||||
private Runnable controlPanelUpdateRunnable;
|
||||
private boolean controlPanelUpdatePending = false;
|
||||
private static final long CONTROL_PANEL_UPDATE_DELAY = 200; // 200ms throttling
|
||||
|
||||
// Переработана функция с оптимизациями
|
||||
private void updateControlPanelPositionSafe() {
|
||||
// Проверки на нулевые размеры (избегаем layout pass)
|
||||
if (compassHeight <= 0) return;
|
||||
if (coordinatesHeight <= 0) return;
|
||||
|
||||
// Изменения только если отличаются от текущих
|
||||
if (params.topMargin != topMargin || params.bottomMargin != bottomMargin) {
|
||||
// Применяем изменения
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. **Безопасные UI обновления**:
|
||||
```java
|
||||
private void updateVesselPositionUI(Vessel vessel) {
|
||||
if (isFinishing() || isDestroyed()) return; // Защита
|
||||
|
||||
runOnUiThread(() -> {
|
||||
try {
|
||||
updateUIActivity(); // Обновляем watchdog
|
||||
// ... безопасные операции
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка в updateVesselPositionUI: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 3. **Дополнительная диагностика**:
|
||||
```java
|
||||
// Добавлен счетчик вызовов updateControlPanelPosition
|
||||
private int controlPanelUpdateCount = 0;
|
||||
|
||||
// Улучшен UI Watchdog с диагностикой handler'ов
|
||||
Log.i(TAG, "UI WATCHDOG: Handler status - " +
|
||||
"watchdog=" + watchdogActive +
|
||||
", controlPanelCount=" + controlPanelUpdateCount);
|
||||
|
||||
// Принудительная остановка при превышении лимита
|
||||
if (controlPanelUpdateCount > 50) {
|
||||
// Останавливаем слишком частые обновления
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **Очистка ресурсов**:
|
||||
```java
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
// Добалена очистка throttling handler'а
|
||||
if (controlPanelUpdateHandler != null) {
|
||||
controlPanelUpdateHandler.removeCallbacks(controlPanelUpdateRunnable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Ожидаемый результат:
|
||||
|
||||
1. **Значительное снижение нагрузки** на главный UI поток
|
||||
2. **Устранение блокировок** от `setLayoutParams()`
|
||||
3. **Throttling обновлений** control panel до безопасного уровня
|
||||
4. **Улучшенная диагностика** для понимания проблем в рантайме
|
||||
5. **Автоматическое восстановление** при превышении лимитов
|
||||
|
||||
## Мониторинг:
|
||||
|
||||
Следите за логами:
|
||||
- `"Control panel updates count: X за последние 10 сек"` - количество обновлений
|
||||
- `"UI WATCHDOG: Handler status"` - состояние всех handler'ов
|
||||
- `"Control panel updated: top=X, bottom=Y"` - фактические обновления
|
||||
|
||||
## Если проблема остается:
|
||||
|
||||
1. Проверьте количество вызовов updateControlPanelPosition
|
||||
2. Рассмотрите полную отключение тестового обновления coordinatesWidget
|
||||
3. Увеличьте CONTROL_PANEL_UPDATE_DELAY до 500мс
|
||||
4. Добавьте дополнительный throttling для BottomSheet обновлений
|
||||
@@ -0,0 +1,95 @@
|
||||
# Оптимизации производительности UI для устранения зависаний
|
||||
|
||||
## Проблемы, которые были исправлены:
|
||||
|
||||
### 1. Избыточные периодические обновления маркеров
|
||||
**Проблема:** YandexMarkerManager обновлял все маркеры каждые 2 секунды
|
||||
**Решение:**
|
||||
- Увеличен интервал до 10 секунд
|
||||
- Изменена логика: теперь проверяется только валидность маркеров, а не полное пересоздание
|
||||
|
||||
### 2. Частые обновления камеры карты
|
||||
**Проблема:** Слушатель камеры срабатывал каждые 50мс
|
||||
**Решение:**
|
||||
- Увеличен throttling до 200мс
|
||||
- Увеличена чувствительность изменения зума с 0.5 до 1.0
|
||||
- Оптимизирована логика обновления маркеров
|
||||
|
||||
### 3. Множественные Handler'ы в UI потоке
|
||||
**Проблема:** Слишком частые обновления UI элементов
|
||||
**Решение:**
|
||||
- MainActivity: интервал обновления сообщений увеличен с 1 до 2 секунд
|
||||
- BottomSheet: интервал обновления увеличен с 1 до 3 секунд
|
||||
- AisTargetsActivity: интервал обновления увеличен с 1 до 2 секунд
|
||||
|
||||
### 4. Частые операции с layout
|
||||
**Проблема:** updateControlPanelPosition() вызывался слишком часто
|
||||
**Решение:**
|
||||
- Добавлен throttling с задержкой 50мс
|
||||
- Добавлена обработка исключений
|
||||
|
||||
### 5. Операции с картой без throttling
|
||||
**Проблема:** Обновления позиции судна на карте без задержки
|
||||
**Решение:**
|
||||
- Добавлен throttling с задержкой 100мс для обновлений карты
|
||||
|
||||
## Дополнительные рекомендации:
|
||||
|
||||
### 1. Мониторинг производительности
|
||||
Добавьте логирование времени выполнения операций:
|
||||
```java
|
||||
long startTime = System.currentTimeMillis();
|
||||
// операция
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
if (duration > 16) { // больше одного кадра (60 FPS)
|
||||
Log.w(TAG, "Медленная операция: " + duration + "мс");
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Оптимизация RecyclerView
|
||||
В AisTargetsAdapter добавьте:
|
||||
```java
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
} else {
|
||||
// Обновляем только измененные поля
|
||||
for (Object payload : payloads) {
|
||||
if ("time_update".equals(payload)) {
|
||||
updateTimeAgo(holder);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Использование ViewStub для тяжелых компонентов
|
||||
Для компонентов, которые не всегда видны, используйте ViewStub.
|
||||
|
||||
### 4. Оптимизация изображений маркеров
|
||||
- Используйте кеширование Bitmap'ов
|
||||
- Предварительно масштабируйте изображения
|
||||
- Используйте hardware acceleration где возможно
|
||||
|
||||
### 5. Мониторинг памяти
|
||||
Добавьте проверки на утечки памяти:
|
||||
```java
|
||||
if (BuildConfig.DEBUG) {
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
long usedMemory = runtime.totalMemory() - runtime.freeMemory();
|
||||
long maxMemory = runtime.maxMemory();
|
||||
if (usedMemory > maxMemory * 0.8) {
|
||||
Log.w(TAG, "Высокое использование памяти: " + (usedMemory / 1024 / 1024) + "MB");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Результат оптимизаций:
|
||||
- Снижена частота обновлений UI в 2-5 раз
|
||||
- Добавлен throttling для предотвращения блокировок
|
||||
- Улучшена обработка ошибок
|
||||
- Снижена нагрузка на главный поток
|
||||
|
||||
Эти изменения должны значительно уменьшить зависания UI и улучшить общую производительность приложения.
|
||||
|
||||
@@ -46,6 +46,16 @@ dependencies {
|
||||
implementation group: 'org.mapsforge', name: 'mapsforge-map-reader', version: '0.25.0'
|
||||
implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.25.0'
|
||||
|
||||
// Room
|
||||
implementation "androidx.room:room-runtime:2.6.1"
|
||||
annotationProcessor "androidx.room:room-compiler:2.6.1"
|
||||
// Lifecycle (для сервисов/репозитория при необходимости)
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime:2.8.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata:2.8.3'
|
||||
|
||||
// MapLibre GL Android SDK (используем только один артефакт, без плагина аннотаций)
|
||||
implementation group: 'org.maplibre.gl', name: 'android-sdk-opengl', version: '11.13.5'
|
||||
|
||||
// Тестирование
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
<!-- Разрешения для GPS -->
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<!-- Для работы в фоне (Android 10+) -->
|
||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||
<!-- Для ForegroundService с локацией (Android 9+) -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<!-- Разрешение на уведомления (Android 13+) -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<!-- Разрешения для интернета (для Яндекс.Карт) -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -13,6 +21,9 @@
|
||||
<!-- Разрешения для UDP -->
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
|
||||
<!-- Разрешения для вибрации -->
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<!-- Разрешения для записи в файл (для логирования) -->
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="28" />
|
||||
@@ -31,12 +42,13 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.AISMap"
|
||||
tools:targetApi="31">
|
||||
|
||||
<profileable android:shell="true" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.AISMap">
|
||||
android:theme="@style/Theme.AISMap"
|
||||
android:keepScreenOn="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
@@ -49,6 +61,18 @@
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.AISMap" />
|
||||
|
||||
<activity
|
||||
android:name=".AisTargetsActivity"
|
||||
android:exported="false"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:theme="@style/Theme.AISMap" />
|
||||
|
||||
<!-- Foreground Service для фоновых обновлений AIS/GPS -->
|
||||
<service
|
||||
android:name=".services.AISForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
|
||||
<!-- Мета-данные для Яндекс.Карт -->
|
||||
<meta-data
|
||||
android:name="com.yandex.mapkit.ApiKey"
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
package com.grigowashere.aismap;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.grigowashere.aismap.data.Repository;
|
||||
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AisTargetsActivity extends AppCompatActivity implements AisTargetsAdapter.OnItemClickListener {
|
||||
|
||||
private Repository repository;
|
||||
private RecyclerView recyclerView;
|
||||
private AisTargetsAdapter adapter;
|
||||
private android.os.Handler tickerHandler;
|
||||
private Runnable tickerRunnable;
|
||||
private android.widget.TextView textEmptyState;
|
||||
private android.widget.TextView textTargetCount;
|
||||
|
||||
// Данные нашего корабля
|
||||
private double ourLatitude = 0;
|
||||
private double ourLongitude = 0;
|
||||
private double ourCourse = 0;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_ais_targets);
|
||||
|
||||
repository = new Repository(this);
|
||||
|
||||
// Загружаем данные нашего корабля
|
||||
loadOurVesselData();
|
||||
|
||||
recyclerView = findViewById(R.id.recycler_ais_targets);
|
||||
textEmptyState = findViewById(R.id.text_empty_state);
|
||||
textTargetCount = findViewById(R.id.text_target_count);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapter = new AisTargetsAdapter(new ArrayList<>(), this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
repository.observeAllAIS().observe(this, new Observer<List<AISVesselEntity>>() {
|
||||
@Override
|
||||
public void onChanged(List<AISVesselEntity> entities) {
|
||||
// Стабильная сортировка по MMSI для предсказуемого порядка
|
||||
if (entities != null) {
|
||||
java.util.Collections.sort(entities, (a, b) -> a.mmsi.compareTo(b.mmsi));
|
||||
}
|
||||
adapter.submitList(entities);
|
||||
|
||||
// Обновляем данные нашего корабля в адаптере
|
||||
adapter.updateOurVesselData(ourLatitude, ourLongitude, ourCourse);
|
||||
|
||||
// Обновляем счетчик целей
|
||||
int targetCount = entities != null ? entities.size() : 0;
|
||||
textTargetCount.setText("AIS цели: " + targetCount);
|
||||
|
||||
// Показываем/скрываем сообщение о пустом состоянии
|
||||
if (entities == null || entities.isEmpty()) {
|
||||
textEmptyState.setVisibility(android.view.View.VISIBLE);
|
||||
recyclerView.setVisibility(android.view.View.GONE);
|
||||
} else {
|
||||
textEmptyState.setVisibility(android.view.View.GONE);
|
||||
recyclerView.setVisibility(android.view.View.VISIBLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Тикер для обновления поля "N сек назад"
|
||||
tickerHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
tickerRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
// Обновляем только элементы с данными, чтобы избежать мигания
|
||||
int itemCount = adapter.getItemCount();
|
||||
for (int i = 0; i < itemCount; i++) {
|
||||
adapter.notifyItemChanged(i, "time_update");
|
||||
}
|
||||
} finally {
|
||||
tickerHandler.postDelayed(this, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
tickerHandler.postDelayed(tickerRunnable, 1000);
|
||||
}
|
||||
|
||||
private void loadOurVesselData() {
|
||||
repository.getLatestOwnVesselAsync(new Repository.RepositoryCallback<VesselEntity>() {
|
||||
@Override
|
||||
public void onComplete(VesselEntity ourVessel) {
|
||||
// Переносим на UI поток для безопасности
|
||||
AisTargetsActivity.this.runOnUiThread(() -> {
|
||||
if (ourVessel != null) {
|
||||
ourLatitude = ourVessel.latitude;
|
||||
ourLongitude = ourVessel.longitude;
|
||||
ourCourse = ourVessel.course;
|
||||
android.util.Log.i("AisTargetsActivity", "Данные нашего корабля загружены: lat=" + ourLatitude +
|
||||
", lon=" + ourLongitude + ", course=" + ourCourse);
|
||||
|
||||
// Обновляем адаптер с новыми данными
|
||||
if (adapter != null) {
|
||||
adapter.updateOurVesselData(ourLatitude, ourLongitude, ourCourse);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w("AisTargetsActivity", "Данные нашего корабля не найдены в БД");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Exception e) {
|
||||
android.util.Log.e("AisTargetsActivity", "Ошибка загрузки данных нашего корабля: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMarinetrafficClick(String mmsi) {
|
||||
String url = "https://www.marinetraffic.com/ru/ais/details/ships/mmsi:" + mmsi;
|
||||
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCenterOnMapClick(String mmsi, double lat, double lon) {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtra("center_mmsi", mmsi);
|
||||
intent.putExtra("center_lat", lat);
|
||||
intent.putExtra("center_lon", lon);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (tickerHandler != null && tickerRunnable != null) {
|
||||
tickerHandler.removeCallbacks(tickerRunnable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.grigowashere.aismap;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||
import com.grigowashere.aismap.utils.NavigationUtils;
|
||||
|
||||
class AisTargetsAdapter extends ListAdapter<AISVesselEntity, AisTargetsAdapter.ViewHolder> {
|
||||
|
||||
interface OnItemClickListener {
|
||||
void onMarinetrafficClick(String mmsi);
|
||||
void onCenterOnMapClick(String mmsi, double lat, double lon);
|
||||
}
|
||||
|
||||
private final OnItemClickListener listener;
|
||||
private double ourLatitude = 0;
|
||||
private double ourLongitude = 0;
|
||||
private double ourCourse = 0;
|
||||
|
||||
protected AisTargetsAdapter(@NonNull DiffUtil.ItemCallback<AISVesselEntity> diffCallback, OnItemClickListener listener) {
|
||||
super(diffCallback);
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public AisTargetsAdapter(java.util.List<AISVesselEntity> initial, OnItemClickListener listener) {
|
||||
this(DIFF_CALLBACK, listener);
|
||||
submitList(initial);
|
||||
}
|
||||
|
||||
public void updateOurVesselData(double latitude, double longitude, double course) {
|
||||
this.ourLatitude = latitude;
|
||||
this.ourLongitude = longitude;
|
||||
this.ourCourse = course;
|
||||
}
|
||||
|
||||
static final DiffUtil.ItemCallback<AISVesselEntity> DIFF_CALLBACK = new DiffUtil.ItemCallback<AISVesselEntity>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull AISVesselEntity oldItem, @NonNull AISVesselEntity newItem) {
|
||||
return oldItem.mmsi.equals(newItem.mmsi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull AISVesselEntity oldItem, @NonNull AISVesselEntity newItem) {
|
||||
return oldItem.latitude == newItem.latitude &&
|
||||
oldItem.longitude == newItem.longitude &&
|
||||
oldItem.course == newItem.course &&
|
||||
oldItem.speed == newItem.speed &&
|
||||
((oldItem.vesselName == null && newItem.vesselName == null) || (oldItem.vesselName != null && oldItem.vesselName.equals(newItem.vesselName)));
|
||||
// Не проверяем lastUpdateEpochMs, чтобы избежать мигания при обновлении времени
|
||||
}
|
||||
};
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_ais_target, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
AISVesselEntity item = getItem(position);
|
||||
holder.bind(item, listener, ourLatitude, ourLongitude, ourCourse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull java.util.List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
} else {
|
||||
// Частичное обновление только времени
|
||||
AISVesselEntity item = getItem(position);
|
||||
holder.updateTimeOnly(item, ourLatitude, ourLongitude, ourCourse);
|
||||
}
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView tvTitle;
|
||||
TextView tvMmsi;
|
||||
TextView tvCoords;
|
||||
TextView tvCourseSpeed;
|
||||
TextView tvLastUpdate;
|
||||
TextView tvTimeAgo;
|
||||
TextView tvDistance;
|
||||
TextView tvBearing;
|
||||
Button btnMarineTraffic;
|
||||
Button btnCenterOnMap;
|
||||
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvTitle = itemView.findViewById(R.id.tv_title);
|
||||
tvMmsi = itemView.findViewById(R.id.tv_mmsi);
|
||||
tvCoords = itemView.findViewById(R.id.tv_coords);
|
||||
tvCourseSpeed = itemView.findViewById(R.id.tv_course_speed);
|
||||
tvLastUpdate = itemView.findViewById(R.id.tv_last_update);
|
||||
tvTimeAgo = itemView.findViewById(R.id.tv_time_ago);
|
||||
tvDistance = itemView.findViewById(R.id.tv_distance);
|
||||
tvBearing = itemView.findViewById(R.id.tv_bearing);
|
||||
btnMarineTraffic = itemView.findViewById(R.id.btn_marine_traffic);
|
||||
btnCenterOnMap = itemView.findViewById(R.id.btn_center_on_map);
|
||||
}
|
||||
|
||||
void bind(AISVesselEntity entity, OnItemClickListener listener, double ourLat, double ourLon, double ourCourse) {
|
||||
String name = entity.vesselName != null && !entity.vesselName.isEmpty() ? entity.vesselName : "MMSI " + entity.mmsi;
|
||||
tvTitle.setText(name);
|
||||
tvMmsi.setText("MMSI: " + entity.mmsi);
|
||||
tvCoords.setText(String.format(java.util.Locale.getDefault(), "%.6f, %.6f", entity.latitude, entity.longitude));
|
||||
tvCourseSpeed.setText(String.format(java.util.Locale.getDefault(), "COG %.1f° • %.1f kn", entity.course, entity.speed));
|
||||
|
||||
// Вычисляем расстояние и азимут
|
||||
if (ourLat != 0 && ourLon != 0 && entity.latitude != 0 && entity.longitude != 0) {
|
||||
double distance = NavigationUtils.calculateDistance(ourLat, ourLon, entity.latitude, entity.longitude);
|
||||
double bearing = NavigationUtils.calculateBearing(ourLat, ourLon, entity.latitude, entity.longitude);
|
||||
double relativeBearing = NavigationUtils.calculateRelativeBearing(ourCourse, bearing);
|
||||
|
||||
tvDistance.setText("Расстояние: " + NavigationUtils.formatDistance(distance));
|
||||
tvBearing.setText("Азимут: " + NavigationUtils.formatRelativeBearing(relativeBearing));
|
||||
} else {
|
||||
tvDistance.setText("Расстояние: --");
|
||||
tvBearing.setText("Азимут: --");
|
||||
}
|
||||
|
||||
// Время последнего обновления и ago
|
||||
if (entity.lastUpdateEpochMs > 0) {
|
||||
java.text.SimpleDateFormat df = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss", java.util.Locale.getDefault());
|
||||
String last = df.format(new java.util.Date(entity.lastUpdateEpochMs));
|
||||
tvLastUpdate.setText("Обновлено: " + last);
|
||||
long secondsAgo = (System.currentTimeMillis() - entity.lastUpdateEpochMs) / 1000L;
|
||||
tvTimeAgo.setText("" + secondsAgo + " сек назад");
|
||||
} else {
|
||||
tvLastUpdate.setText("Обновлено: --");
|
||||
tvTimeAgo.setText("-- сек назад");
|
||||
}
|
||||
btnMarineTraffic.setOnClickListener(v -> listener.onMarinetrafficClick(entity.mmsi));
|
||||
btnCenterOnMap.setOnClickListener(v -> {
|
||||
android.util.Log.i("AisTargetsAdapter", "Кнопка 'На карте' нажата для MMSI=" + entity.mmsi + ", lat=" + entity.latitude + ", lon=" + entity.longitude);
|
||||
listener.onCenterOnMapClick(entity.mmsi, entity.latitude, entity.longitude);
|
||||
});
|
||||
}
|
||||
|
||||
void updateTimeOnly(AISVesselEntity entity, double ourLat, double ourLon, double ourCourse) {
|
||||
// Обновляем только поля времени, чтобы избежать мигания всего элемента
|
||||
if (entity.lastUpdateEpochMs > 0) {
|
||||
java.text.SimpleDateFormat df = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss", java.util.Locale.getDefault());
|
||||
String last = df.format(new java.util.Date(entity.lastUpdateEpochMs));
|
||||
tvLastUpdate.setText("Обновлено: " + last);
|
||||
long secondsAgo = (System.currentTimeMillis() - entity.lastUpdateEpochMs) / 1000L;
|
||||
tvTimeAgo.setText("" + secondsAgo + " сек назад");
|
||||
} else {
|
||||
tvLastUpdate.setText("Обновлено: --");
|
||||
tvTimeAgo.setText("-- сек назад");
|
||||
}
|
||||
|
||||
// Также обновляем расстояние и азимут при обновлении времени
|
||||
if (ourLat != 0 && ourLon != 0 && entity.latitude != 0 && entity.longitude != 0) {
|
||||
double distance = NavigationUtils.calculateDistance(ourLat, ourLon, entity.latitude, entity.longitude);
|
||||
double bearing = NavigationUtils.calculateBearing(ourLat, ourLon, entity.latitude, entity.longitude);
|
||||
double relativeBearing = NavigationUtils.calculateRelativeBearing(ourCourse, bearing);
|
||||
|
||||
tvDistance.setText("Расстояние: " + NavigationUtils.formatDistance(distance));
|
||||
tvBearing.setText("Азимут: " + NavigationUtils.formatRelativeBearing(relativeBearing));
|
||||
} else {
|
||||
tvDistance.setText("Расстояние: --");
|
||||
tvBearing.setText("Азимут: --");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,23 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
private RadioButton radioHybridMode;
|
||||
private RadioButton radioNMEAOnly;
|
||||
private RadioButton radioAndroidOnly;
|
||||
private EditText etStaleWarningMinutes;
|
||||
private EditText etStaleRemoveMinutes;
|
||||
private SwitchMaterial switchVibrationEnabled;
|
||||
private SwitchMaterial switchSoundEnabled;
|
||||
private SwitchMaterial switchKeepScreenOn;
|
||||
private SwitchMaterial switchCursorEnabled;
|
||||
private Button btnCancel;
|
||||
private Button btnSave;
|
||||
private Button btnClearPath;
|
||||
|
||||
// Path/prediction
|
||||
private EditText etPathMaxPoints;
|
||||
private EditText etPathWidth;
|
||||
private EditText etPathColor;
|
||||
private EditText etPredictionWidth;
|
||||
private EditText etPredictionColor;
|
||||
private EditText etPredictionHorizon;
|
||||
|
||||
// Состояние настроек до изменений
|
||||
private int originalUDPPort;
|
||||
@@ -42,6 +57,12 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
private boolean originalAndroidNMEAEnabled;
|
||||
private boolean originalUDPNMEAEnabled;
|
||||
private String originalDataMode;
|
||||
private int originalStaleWarningMinutes;
|
||||
private int originalStaleRemoveMinutes;
|
||||
private boolean originalVibrationEnabled;
|
||||
private boolean originalSoundEnabled;
|
||||
private boolean originalKeepScreenOnEnabled;
|
||||
private boolean originalCursorEnabled;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -78,8 +99,22 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
radioHybridMode = findViewById(R.id.radio_hybrid_mode);
|
||||
radioNMEAOnly = findViewById(R.id.radio_nmea_only);
|
||||
radioAndroidOnly = findViewById(R.id.radio_android_only);
|
||||
etStaleWarningMinutes = findViewById(R.id.et_stale_warning_minutes);
|
||||
etStaleRemoveMinutes = findViewById(R.id.et_stale_remove_minutes);
|
||||
switchVibrationEnabled = findViewById(R.id.switch_vibration_enabled);
|
||||
switchSoundEnabled = findViewById(R.id.switch_sound_enabled);
|
||||
switchKeepScreenOn = findViewById(R.id.switch_keep_screen_on);
|
||||
switchCursorEnabled = findViewById(R.id.switch_cursor_enabled);
|
||||
btnCancel = findViewById(R.id.btn_cancel);
|
||||
btnSave = findViewById(R.id.btn_save);
|
||||
btnClearPath = findViewById(R.id.btn_clear_path);
|
||||
|
||||
etPathMaxPoints = findViewById(R.id.et_path_max_points);
|
||||
etPathWidth = findViewById(R.id.et_path_width);
|
||||
etPathColor = findViewById(R.id.et_path_color);
|
||||
etPredictionWidth = findViewById(R.id.et_prediction_width);
|
||||
etPredictionColor = findViewById(R.id.et_prediction_color);
|
||||
etPredictionHorizon = findViewById(R.id.et_prediction_horizon_sec);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,6 +143,28 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
break;
|
||||
}
|
||||
|
||||
// Настройки устаревания данных
|
||||
etStaleWarningMinutes.setText(String.valueOf(settingsManager.getDataStaleWarningMinutes()));
|
||||
etStaleRemoveMinutes.setText(String.valueOf(settingsManager.getDataStaleRemoveMinutes()));
|
||||
|
||||
// Настройки уведомлений
|
||||
switchVibrationEnabled.setChecked(settingsManager.isVibrationEnabled());
|
||||
switchSoundEnabled.setChecked(settingsManager.isSoundEnabled());
|
||||
|
||||
// Настройки экрана
|
||||
switchKeepScreenOn.setChecked(settingsManager.isKeepScreenOnEnabled());
|
||||
|
||||
// Настройки курсора
|
||||
switchCursorEnabled.setChecked(settingsManager.isCursorEnabled());
|
||||
|
||||
// Путь и предсказание
|
||||
etPathMaxPoints.setText(String.valueOf(settingsManager.getPathMaxPoints()));
|
||||
etPathWidth.setText(String.valueOf(settingsManager.getPathWidth()));
|
||||
etPathColor.setText(String.format("#%06X", (0xFFFFFF & settingsManager.getPathColor())));
|
||||
etPredictionWidth.setText(String.valueOf(settingsManager.getPredictionWidth()));
|
||||
etPredictionColor.setText(String.format("#%06X", (0xFFFFFF & settingsManager.getPredictionColor())));
|
||||
etPredictionHorizon.setText(String.valueOf(settingsManager.getPredictionHorizonSec()));
|
||||
|
||||
Log.i(TAG, "Настройки загружены в UI");
|
||||
}
|
||||
|
||||
@@ -120,6 +177,12 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
originalAndroidNMEAEnabled = settingsManager.isAndroidNMEAEnabled();
|
||||
originalUDPNMEAEnabled = settingsManager.isUDPNMEAEnabled();
|
||||
originalDataMode = settingsManager.getDataMode();
|
||||
originalStaleWarningMinutes = settingsManager.getDataStaleWarningMinutes();
|
||||
originalStaleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes();
|
||||
originalVibrationEnabled = settingsManager.isVibrationEnabled();
|
||||
originalSoundEnabled = settingsManager.isSoundEnabled();
|
||||
originalKeepScreenOnEnabled = settingsManager.isKeepScreenOnEnabled();
|
||||
originalCursorEnabled = settingsManager.isCursorEnabled();
|
||||
|
||||
Log.i(TAG, "Оригинальные настройки сохранены");
|
||||
}
|
||||
@@ -140,6 +203,12 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
saveSettings();
|
||||
});
|
||||
|
||||
// Кнопка очистки пути
|
||||
btnClearPath.setOnClickListener(v -> {
|
||||
Log.i(TAG, "Нажата кнопка очистки пути");
|
||||
clearVesselPath();
|
||||
});
|
||||
|
||||
// Обработчик изменения режима данных
|
||||
radioGroupDataMode.setOnCheckedChangeListener((group, checkedId) -> {
|
||||
updateDataModeDescription();
|
||||
@@ -221,12 +290,39 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
// Валидируем настройки устаревания данных
|
||||
int staleWarningMinutes = validateStaleMinutes(etStaleWarningMinutes.getText().toString().trim(), "время предупреждения");
|
||||
if (staleWarningMinutes == -1) return;
|
||||
|
||||
int staleRemoveMinutes = validateStaleMinutes(etStaleRemoveMinutes.getText().toString().trim(), "время удаления");
|
||||
if (staleRemoveMinutes == -1) return;
|
||||
|
||||
// Проверяем логичность значений
|
||||
if (staleWarningMinutes >= staleRemoveMinutes) {
|
||||
Toast.makeText(this, "Время предупреждения должно быть меньше времени удаления", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем настройки
|
||||
settingsManager.setUDPPort(udpPort);
|
||||
settingsManager.setUDPEnabled(switchUDPEnabled.isChecked());
|
||||
settingsManager.setAndroidNMEAEnabled(switchAndroidNMEAEnabled.isChecked());
|
||||
settingsManager.setUDPNMEAEnabled(switchUDPNMEAEnabled.isChecked());
|
||||
settingsManager.setDataMode(dataMode);
|
||||
settingsManager.setDataStaleWarningMinutes(staleWarningMinutes);
|
||||
settingsManager.setDataStaleRemoveMinutes(staleRemoveMinutes);
|
||||
settingsManager.setVibrationEnabled(switchVibrationEnabled.isChecked());
|
||||
settingsManager.setSoundEnabled(switchSoundEnabled.isChecked());
|
||||
settingsManager.setKeepScreenOnEnabled(switchKeepScreenOn.isChecked());
|
||||
settingsManager.setCursorEnabled(switchCursorEnabled.isChecked());
|
||||
|
||||
// Путь и предсказание
|
||||
try { settingsManager.setPathMaxPoints(Integer.parseInt(etPathMaxPoints.getText().toString().trim())); } catch (Exception ignored) {}
|
||||
try { settingsManager.setPathWidth(Float.parseFloat(etPathWidth.getText().toString().trim())); } catch (Exception ignored) {}
|
||||
try { settingsManager.setPathColor(parseColor(etPathColor.getText().toString().trim(), settingsManager.getPathColor())); } catch (Exception ignored) {}
|
||||
try { settingsManager.setPredictionWidth(Float.parseFloat(etPredictionWidth.getText().toString().trim())); } catch (Exception ignored) {}
|
||||
try { settingsManager.setPredictionColor(parseColor(etPredictionColor.getText().toString().trim(), settingsManager.getPredictionColor())); } catch (Exception ignored) {}
|
||||
try { settingsManager.setPredictionHorizonSec(Integer.parseInt(etPredictionHorizon.getText().toString().trim())); } catch (Exception ignored) {}
|
||||
|
||||
Log.i(TAG, "Настройки сохранены: " + settingsManager.getSettingsSummary());
|
||||
|
||||
@@ -242,6 +338,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
resultIntent.putExtra("android_nmea_enabled", switchAndroidNMEAEnabled.isChecked());
|
||||
resultIntent.putExtra("udp_nmea_enabled", switchUDPNMEAEnabled.isChecked());
|
||||
resultIntent.putExtra("data_mode", dataMode);
|
||||
resultIntent.putExtra("cursor_enabled", switchCursorEnabled.isChecked());
|
||||
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
|
||||
@@ -254,6 +351,20 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private int parseColor(String text, int fallback) {
|
||||
try {
|
||||
if (text == null || text.isEmpty()) return fallback;
|
||||
String s = text.startsWith("#") ? text : ("#" + text);
|
||||
// добавим полную непрозрачность если пришёл #RRGGBB
|
||||
if (s.length() == 7) {
|
||||
s = "#FF" + s.substring(1);
|
||||
}
|
||||
return (int) Long.parseLong(s.substring(1), 16);
|
||||
} catch (Exception e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает выбранный режим данных
|
||||
*/
|
||||
@@ -307,6 +418,28 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Валидирует время устаревания данных
|
||||
*/
|
||||
private int validateStaleMinutes(String text, String fieldName) {
|
||||
if (text.isEmpty()) {
|
||||
Toast.makeText(this, fieldName + " не может быть пустым", Toast.LENGTH_SHORT).show();
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
int minutes = Integer.parseInt(text);
|
||||
if (minutes < 1 || minutes > 60) {
|
||||
Toast.makeText(this, fieldName + " должно быть от 1 до 60 минут", Toast.LENGTH_SHORT).show();
|
||||
return -1;
|
||||
}
|
||||
return minutes;
|
||||
} catch (NumberFormatException e) {
|
||||
Toast.makeText(this, "Некорректный формат " + fieldName, Toast.LENGTH_SHORT).show();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли перезапустить сервисы
|
||||
*/
|
||||
@@ -315,6 +448,28 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
settingsManager.shouldRestartNMEA(originalAndroidNMEAEnabled, originalUDPNMEAEnabled, originalDataMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает трекер пути собственного судна
|
||||
*/
|
||||
private void clearVesselPath() {
|
||||
try {
|
||||
// Создаем интент для уведомления MainActivity об очистке пути
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra("clear_vessel_path", true);
|
||||
setResult(RESULT_OK, resultIntent);
|
||||
|
||||
Toast.makeText(this, "Трекер пути будет очищен", Toast.LENGTH_SHORT).show();
|
||||
Log.i(TAG, "Запрошена очистка трекера пути");
|
||||
|
||||
// Закрываем SettingsActivity чтобы передать результат в MainActivity
|
||||
finish();
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при очистке пути: " + e.getMessage(), e);
|
||||
Toast.makeText(this, "Ошибка при очистке пути", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
Log.i(TAG, "Нажата кнопка назад");
|
||||
|
||||
@@ -5,8 +5,15 @@ import android.util.Log;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.maps.MapInterface;
|
||||
import com.grigowashere.aismap.data.Repository;
|
||||
import com.grigowashere.aismap.data.mapper.AISVesselMapper;
|
||||
import com.grigowashere.aismap.services.NotificationService;
|
||||
import com.grigowashere.aismap.utils.SettingsManager;
|
||||
import com.grigowashere.aismap.ui.UIDataChangeNotifier;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@@ -34,6 +41,12 @@ public class AppController implements
|
||||
private Vessel ownVessel;
|
||||
private List<AISVessel> aisVessels;
|
||||
private ExecutorService executor;
|
||||
private com.grigowashere.aismap.data.Repository repository;
|
||||
private NotificationService notificationService;
|
||||
private SettingsManager settingsManager;
|
||||
private VesselPathController pathController;
|
||||
// VesselPathController для каждого AIS судна (ключ: MMSI)
|
||||
private final Map<String, VesselPathController> aisPathControllers = new HashMap<>();
|
||||
|
||||
private boolean isUDPEnabled;
|
||||
private boolean isAndroidNMEAEnabled;
|
||||
@@ -42,9 +55,27 @@ public class AppController implements
|
||||
private int udpPort;
|
||||
private String dataMode;
|
||||
|
||||
// Callback для обновления UI
|
||||
// Время последнего получения сообщений ($ GPS) и (! AIS) в elapsedRealtime
|
||||
private long lastGPSMessageRealtimeMs;
|
||||
private long lastAISMessageRealtimeMs;
|
||||
|
||||
// Периодическая очистка БД от устаревших AIS целей
|
||||
private android.os.Handler dbCleanupHandler;
|
||||
private Runnable dbCleanupRunnable;
|
||||
private static final long DB_CLEANUP_INTERVAL = 60000; // 1 минута
|
||||
|
||||
// Единый Handler для всех UI операций (предотвращение утечек Handler'ов)
|
||||
private android.os.Handler uiHandler;
|
||||
|
||||
// Индикаторы UI данных для централизованного throttling
|
||||
private UIDataChangeNotifier uiDataNotifier;
|
||||
|
||||
// Callback для обновления UI (legacy для MainActivity)
|
||||
private UIUpdateCallback uiUpdateCallback;
|
||||
|
||||
// Диагностика сервисов
|
||||
private long lastServiceLogTime = 0;
|
||||
|
||||
public interface UIUpdateCallback {
|
||||
void onVesselPositionUpdated(Vessel vessel);
|
||||
void onGPSQualityUpdated(Vessel vessel);
|
||||
@@ -64,6 +95,17 @@ public class AppController implements
|
||||
this.ownVessel = new Vessel();
|
||||
this.aisVessels = new ArrayList<>();
|
||||
this.executor = Executors.newCachedThreadPool();
|
||||
this.repository = new com.grigowashere.aismap.data.Repository(context);
|
||||
this.notificationService = new NotificationService(context);
|
||||
this.settingsManager = new SettingsManager(context);
|
||||
this.pathController = new VesselPathController(context, settingsManager);
|
||||
|
||||
// Инициализируем Handler для периодической очистки БД
|
||||
this.dbCleanupHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
this.dbCleanupRunnable = this::performDatabaseCleanup;
|
||||
|
||||
// Инициализируем единый UI Handler
|
||||
this.uiHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||
|
||||
initializeControllers();
|
||||
}
|
||||
@@ -92,6 +134,55 @@ public class AppController implements
|
||||
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
|
||||
androidNmeaListener = new AndroidNMEAListener(context);
|
||||
androidNmeaListener.setCallback(this);
|
||||
|
||||
// Восстанавливаем данные из БД при старте АСИНХРОННО
|
||||
Log.i(TAG, "🔄 Запускаем асинхронное восстановление данных из БД...");
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
Log.d(TAG, "📊 Загружаем данные судна из БД...");
|
||||
com.grigowashere.aismap.data.entity.VesselEntity latest = repository.getLatestOwnVesselSync();
|
||||
if (latest != null) {
|
||||
ownVessel.setLatitude(latest.latitude);
|
||||
ownVessel.setLongitude(latest.longitude);
|
||||
ownVessel.setAccuracy(latest.accuracy);
|
||||
ownVessel.setFixTime(latest.fixTime);
|
||||
Log.d(TAG, "✅ Данные судна восстановлены: " + latest.latitude + "," + latest.longitude);
|
||||
} else {
|
||||
Log.d(TAG, "ℹ️ Нет данных судна в БД");
|
||||
}
|
||||
|
||||
Log.d(TAG, "🚢 Загружаем AIS суда из БД...");
|
||||
java.util.List<com.grigowashere.aismap.data.entity.AISVesselEntity> list = repository.getAllAISSync();
|
||||
if (list != null && !list.isEmpty()) {
|
||||
synchronized (aisVessels) {
|
||||
aisVessels.clear(); // Очищаем перед восстановлением
|
||||
for (com.grigowashere.aismap.data.entity.AISVesselEntity entity : list) {
|
||||
// Используем маппер для полного восстановления всех полей
|
||||
AISVessel vessel = AISVesselMapper.toModel(entity);
|
||||
aisVessels.add(vessel);
|
||||
Log.d(TAG, "AIS судно восстановлено из БД с полными данными: " + vessel.getMmsi());
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "✅ Восстановлено " + list.size() + " AIS судов из БД с полными данными");
|
||||
} else {
|
||||
Log.d(TAG, "ℹ️ Нет AIS судов в БД");
|
||||
}
|
||||
|
||||
// Уведомляем UI о восстановлении данных (если mapInterface уже установлен)
|
||||
uiHandler.post(() -> {
|
||||
if (mapInterface != null) {
|
||||
Log.i(TAG, "🔄 Уведомляем UI о восстановленных данных...");
|
||||
// Восстановление маркеров будет выполнено через setMapInterface()
|
||||
// когда он будет вызван из MainActivity
|
||||
} else {
|
||||
Log.d(TAG, "⏳ mapInterface еще не установлен, восстановление отложено");
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "❌ Ошибка восстановления данных из БД: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,11 +195,46 @@ public class AppController implements
|
||||
Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface");
|
||||
mapInterface.setMarkerClickListener(this);
|
||||
Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры");
|
||||
|
||||
// Уведомляем UI Coordinator о восстановлении данных
|
||||
if (uiDataNotifier != null) {
|
||||
Log.i(TAG, "🔄 Восстановление данных через UI Coordinator");
|
||||
|
||||
// Восстанавливаем позицию собственного судна
|
||||
if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) {
|
||||
Log.i(TAG, "📍 Восстанавливаем позицию судна: " + ownVessel.getLatitude() + "," + ownVessel.getLongitude());
|
||||
uiDataNotifier.onVesselPositionChanged(ownVessel);
|
||||
} else {
|
||||
Log.w(TAG, "⚠️ Судно не имеет валидных координат для восстановления");
|
||||
}
|
||||
|
||||
// Восстанавливаем AIS суда
|
||||
if (aisVessels != null && !aisVessels.isEmpty()) {
|
||||
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
|
||||
for (AISVessel v : aisVessels) {
|
||||
Log.d(TAG, " - AIS судно: " + v.getMmsi() + " на " + v.getLatitude() + "," + v.getLongitude());
|
||||
uiDataNotifier.onAISVesselChanged(v);
|
||||
}
|
||||
Log.i(TAG, "✅ " + aisVessels.size() + " AIS судов отправлено в UI Coordinator");
|
||||
} else {
|
||||
Log.i(TAG, "ℹ️ Нет AIS судов для восстановления");
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "❌ uiDataNotifier не установлен при восстановлении данных - маркеры НЕ будут восстановлены!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает callback для обновления UI
|
||||
* Устанавливает индикатор изменений данных для централизованного UI throttling
|
||||
*/
|
||||
public void setUIDataChangeNotifier(UIDataChangeNotifier notifier) {
|
||||
this.uiDataNotifier = notifier;
|
||||
Log.i(TAG, "UIDataChangeNotifier установлен: " + (notifier != null ? "success" : "null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает callback для обновления UI (legacy для MainActivity)
|
||||
*/
|
||||
public void setUIUpdateCallback(UIUpdateCallback callback) {
|
||||
this.uiUpdateCallback = callback;
|
||||
@@ -135,7 +261,8 @@ public class AppController implements
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Запускаем периодическую очистку БД от устаревших AIS целей
|
||||
startDatabaseCleanup();
|
||||
}
|
||||
|
||||
|
||||
@@ -144,6 +271,9 @@ public class AppController implements
|
||||
* Останавливает все слушатели
|
||||
*/
|
||||
public void stopAllListeners() {
|
||||
// Останавливаем периодическую очистку БД
|
||||
stopDatabaseCleanup();
|
||||
|
||||
executor.execute(() -> {
|
||||
udpListener.stop();
|
||||
androidNmeaListener.stopListening();
|
||||
@@ -279,23 +409,41 @@ public class AppController implements
|
||||
ownVessel.setFixTime(vessel.getFixTime());
|
||||
ownVessel.setFixQuality(vessel.getFixQuality());
|
||||
|
||||
// Добавляем точку в путь судна
|
||||
if (pathController != null) {
|
||||
boolean pointAdded = pathController.addPathPoint(
|
||||
vessel.getLongitude(),
|
||||
vessel.getLatitude(),
|
||||
(float) ownVessel.getSpeed()
|
||||
);
|
||||
if (pointAdded) {
|
||||
Log.d(TAG, "Точка пути добавлена из GPS: " + pathController.getPathPointsCount() + " точек");
|
||||
}
|
||||
}
|
||||
|
||||
// Сохраняем позицию в локальную БД
|
||||
try {
|
||||
com.grigowashere.aismap.data.entity.VesselEntity ve = new com.grigowashere.aismap.data.entity.VesselEntity();
|
||||
ve.latitude = ownVessel.getLatitude();
|
||||
ve.longitude = ownVessel.getLongitude();
|
||||
ve.accuracy = ownVessel.getAccuracy();
|
||||
ve.fixTime = ownVessel.getFixTime();
|
||||
repository.upsertOwnVessel(ve);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка сохранения позиции в БД: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Обновляем 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);
|
||||
}
|
||||
});
|
||||
// Уведомляем UI Coordinator об изменении позиции судна (централизованный throttling)
|
||||
if (uiDataNotifier != null) {
|
||||
Log.d(TAG, "Уведомляем UI Coordinator об изменении позиции судна");
|
||||
uiDataNotifier.onVesselPositionChanged(ownVessel);
|
||||
} else {
|
||||
Log.w(TAG, "uiDataNotifier не установлен, пропускаем UI обновление");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,17 +456,23 @@ public class AppController implements
|
||||
|
||||
@Override
|
||||
public void onVesselUpdated(Vessel vessel) {
|
||||
Log.i(TAG, "🔄 onVesselUpdated вызван: lat=" + vessel.getLatitude() +
|
||||
", lon=" + vessel.getLongitude() +
|
||||
", course=" + vessel.getCourse() +
|
||||
", speed=" + vessel.getSpeed());
|
||||
// Сокращаем шум логов: подробности обновления судна убраны
|
||||
|
||||
// Обновляем координаты, если они есть (для режима "только NMEA")
|
||||
if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) {
|
||||
ownVessel.setLatitude(vessel.getLatitude());
|
||||
ownVessel.setLongitude(vessel.getLongitude());
|
||||
Log.i(TAG, "📍 Координаты обновлены из NMEA: lat=" + vessel.getLatitude() +
|
||||
", lon=" + vessel.getLongitude());
|
||||
// Сокращаем шум логов: координаты обновлены (без детализации)
|
||||
|
||||
// Добавляем точку в путь судна
|
||||
if (pathController != null) {
|
||||
boolean pointAdded = pathController.addPathPoint(
|
||||
vessel.getLongitude(),
|
||||
vessel.getLatitude(),
|
||||
(float) vessel.getSpeed()
|
||||
);
|
||||
// Убираем лог о добавлении каждой точки пути
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем дополнительные данные
|
||||
@@ -336,18 +490,14 @@ public class AppController implements
|
||||
ownVessel.setAltitude(vessel.getAltitude());
|
||||
}
|
||||
|
||||
Log.i(TAG, "NMEA данные обновлены: course=" + vessel.getCourse() +
|
||||
", speed=" + vessel.getSpeed() +
|
||||
", satellites=" + vessel.getSatellites());
|
||||
// Сокращаем шум логов: сводка NMEA обновлений убрана
|
||||
|
||||
// Обновляем карту в главном потоке
|
||||
if (mapInterface != null) {
|
||||
Log.i(TAG, "Обновляем позицию на карте из NMEA...");
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
// Сокращаем шум логов: убираем информационные логи карты
|
||||
uiHandler.post(() -> {
|
||||
try {
|
||||
Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition из NMEA...");
|
||||
mapInterface.updateOwnVesselPosition(ownVessel);
|
||||
Log.i(TAG, "Позиция на карте обновлена из NMEA");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка обновления позиции на карте из NMEA: " + e.getMessage(), e);
|
||||
}
|
||||
@@ -362,7 +512,7 @@ public class AppController implements
|
||||
|
||||
@Override
|
||||
public void onDOPUpdated(double pdop, double hdop, double vdop) {
|
||||
Log.i(TAG, "📊 DOP обновлен: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
|
||||
// Убираем шумный лог DOP обновлений
|
||||
|
||||
// Обновляем DOP значения
|
||||
ownVessel.setPdop(pdop);
|
||||
@@ -381,6 +531,17 @@ public class AppController implements
|
||||
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
|
||||
|
||||
if (existingVessel != null) {
|
||||
// Если пришло новое safety-сообщение (тип 14), уведомим пользователя
|
||||
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
|
||||
String prev = existingVessel.getLastSafetyMessage();
|
||||
String curr = vessel.getLastSafetyMessage();
|
||||
if (prev == null || !prev.equals(curr)) {
|
||||
if (notificationService != null && notificationService.areNotificationsEnabled()) {
|
||||
notificationService.notifySafetyMessage(vessel.getMmsi(), curr);
|
||||
}
|
||||
}
|
||||
existingVessel.setLastSafetyMessage(curr);
|
||||
}
|
||||
// Обновляем существующее судно
|
||||
existingVessel.updatePosition(
|
||||
vessel.getLatitude(),
|
||||
@@ -388,30 +549,60 @@ public class AppController implements
|
||||
vessel.getCourse(),
|
||||
vessel.getSpeed()
|
||||
);
|
||||
|
||||
if (mapInterface != null) {
|
||||
// Используем Handler для выполнения в главном потоке
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
try {
|
||||
mapInterface.updateAISVesselPosition(existingVessel);
|
||||
// Используем маппер для полной конвертации всех полей
|
||||
com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(existingVessel);
|
||||
repository.upsertAIS(entity);
|
||||
Log.d(TAG, "AIS судно сохранено в БД с полными данными: " + existingVessel.getMmsi());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка обновления позиции AIS судна на карте: " + e.getMessage(), e);
|
||||
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем точку в путь AIS судна
|
||||
addAISVesselPathPoint(existingVessel);
|
||||
|
||||
// Уведомляем UI Coordinator об обновлении AIS судна
|
||||
if (uiDataNotifier != null) {
|
||||
Log.d(TAG, "Уведомляем UI Coordinator об обновлении AIS судна: " + existingVessel.getMmsi());
|
||||
uiDataNotifier.onAISVesselChanged(existingVessel);
|
||||
} else {
|
||||
Log.w(TAG, "uiDataNotifier не установлен, пропускаем AIS обновление");
|
||||
}
|
||||
} 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);
|
||||
// Если это новое судно сразу пришло с safety-сообщением — уведомим
|
||||
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
|
||||
if (notificationService != null && notificationService.areNotificationsEnabled()) {
|
||||
notificationService.notifySafetyMessage(vessel.getMmsi(), vessel.getLastSafetyMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Воспроизводим уведомление о новой цели
|
||||
if (notificationService != null && notificationService.areNotificationsEnabled()) {
|
||||
notificationService.notifyNewAISTarget();
|
||||
Log.i(TAG, "🔔 Уведомление о новой AIS цели: " + vessel.getMmsi());
|
||||
}
|
||||
|
||||
try {
|
||||
// Используем маппер для полной конвертации всех полей
|
||||
com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(vessel);
|
||||
repository.upsertAIS(entity);
|
||||
Log.d(TAG, "Новое AIS судно сохранено в БД с полными данными: " + vessel.getMmsi());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Добавляем точку в путь нового AIS судна
|
||||
addAISVesselPathPoint(vessel);
|
||||
|
||||
// Уведомляем UI Coordinator о новом AIS судне
|
||||
if (uiDataNotifier != null) {
|
||||
Log.d(TAG, "Уведомляем UI Coordinator о новом AIS судне: " + vessel.getMmsi());
|
||||
uiDataNotifier.onAISVesselChanged(vessel);
|
||||
} else {
|
||||
Log.w(TAG, "uiDataNotifier не установлен, пропускаем добавление AIS судна");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -434,7 +625,8 @@ public class AppController implements
|
||||
float azimuth = (float) ownVessel.getCourse();
|
||||
List<AISVessel> nearbyVessels = getNearbyVessels();
|
||||
|
||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||
// Используем существующий uiHandler вместо создания нового
|
||||
uiHandler.post(() -> {
|
||||
((ExtendedUIUpdateCallback) uiUpdateCallback).onUpdateCompass(azimuth, nearbyVessels);
|
||||
});
|
||||
}
|
||||
@@ -461,10 +653,30 @@ public class AppController implements
|
||||
|
||||
@Override
|
||||
public void onDataReceived(String data, String sourceAddress, int sourcePort) {
|
||||
Log.d(TAG, "UDP данные получены от " + sourceAddress + ":" + sourcePort);
|
||||
// Диагностика: логируем каждые 10 секунд
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastServiceLogTime > 10000) {
|
||||
Log.d(TAG, "📡 AppController: UDP данные получены от " + sourceAddress + ":" + sourcePort);
|
||||
lastServiceLogTime = now;
|
||||
}
|
||||
|
||||
// Парсим полученные данные как NMEA
|
||||
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
nmeaParser.parseNMEA(data);
|
||||
// Диагностика: логируем каждые 10 секунд
|
||||
long now2 = System.currentTimeMillis();
|
||||
if (now2 - lastServiceLogTime > 10000) {
|
||||
Log.d(TAG, "✅ AppController: UDP NMEA обработано в фоновом потоке");
|
||||
lastServiceLogTime = now2;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "❌ Ошибка парсинга UDP NMEA в фоновом потоке: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем метки времени по префиксу в UI потоке (быстрая операция)
|
||||
updateLastMessageAgesFromRaw(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -481,10 +693,41 @@ public class AppController implements
|
||||
|
||||
@Override
|
||||
public void onNMEAMessage(String message, long timestamp) {
|
||||
Log.i(TAG, "📱 Android NMEA сообщение получено в AppController: " + message);
|
||||
// Диагностика: логируем каждые 10 секунд
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastServiceLogTime > 10000) {
|
||||
Log.d(TAG, "📱 AppController: Android NMEA сообщение получено");
|
||||
lastServiceLogTime = now;
|
||||
}
|
||||
|
||||
// Парсим полученные данные как NMEA
|
||||
// Парсим полученные данные как NMEA В ФОНОВОМ ПОТОКЕ
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
nmeaParser.parseNMEA(message);
|
||||
// Диагностика: логируем каждые 10 секунд
|
||||
long now2 = System.currentTimeMillis();
|
||||
if (now2 - lastServiceLogTime > 10000) {
|
||||
Log.d(TAG, "✅ AppController: NMEA обработано в фоновом потоке");
|
||||
lastServiceLogTime = now2;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "❌ Ошибка парсинга NMEA в фоновом потоке: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем метки времени в UI потоке (быстрая операция)
|
||||
if (message != null) {
|
||||
String trimmed = message.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
char c = trimmed.charAt(0);
|
||||
long now3 = android.os.SystemClock.elapsedRealtime();
|
||||
if (c == '$') {
|
||||
lastGPSMessageRealtimeMs = now3;
|
||||
} else if (c == '!') {
|
||||
lastAISMessageRealtimeMs = now3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Реализация MarkerClickListener
|
||||
@@ -548,32 +791,82 @@ public class AppController implements
|
||||
* Очищает все AIS суда
|
||||
*/
|
||||
public void clearAISVessels() {
|
||||
Log.i(TAG, "Очищаем AIS суда из контроллера");
|
||||
|
||||
// Очищаем локальные данные
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// Уведомляем UI Coordinator о необходимости очистки карты
|
||||
if (uiDataNotifier != null) {
|
||||
Log.d(TAG, "Уведомляем UI Coordinator об очистке AIS судов");
|
||||
// TODO: Добавить метод очистки всех AIS судов в UIDataChangeNotifier
|
||||
// Пока что очищаем через individual removals
|
||||
Log.i(TAG, "Individual AIS removal через uiDataNotifier еще не реализован");
|
||||
} else {
|
||||
Log.w(TAG, "uiDataNotifier не установлен, очистка AIS судов пропущена");
|
||||
}
|
||||
|
||||
// Очищаем AIS path controllers
|
||||
aisPathControllers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Центрирует карту на позиции нашего судна
|
||||
*/
|
||||
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);
|
||||
if (ownVessel != null) {
|
||||
Log.d(TAG, "Запрос центрирования карты на судне: " + ownVessel.getLatitude() + "," + ownVessel.getLongitude());
|
||||
|
||||
// Уведомляем UI Coordinator о необходимости центрирования карты
|
||||
if (uiDataNotifier != null) {
|
||||
uiDataNotifier.onRequestCenterMap(ownVessel.getLatitude(), ownVessel.getLongitude());
|
||||
} else {
|
||||
Log.w(TAG, "uiDataNotifier не установлен, центрирование карты пропущено");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает периодическую очистку БД от устаревших AIS целей
|
||||
*/
|
||||
public void startDatabaseCleanup() {
|
||||
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
|
||||
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
|
||||
Log.i(TAG, "Запущена периодическая очистка БД от устаревших AIS целей");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает периодическую очистку БД
|
||||
*/
|
||||
public void stopDatabaseCleanup() {
|
||||
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
|
||||
dbCleanupHandler.removeCallbacks(dbCleanupRunnable);
|
||||
Log.i(TAG, "Остановлена периодическая очистка БД от устаревших AIS целей");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполняет очистку БД от устаревших AIS целей
|
||||
*/
|
||||
private void performDatabaseCleanup() {
|
||||
try {
|
||||
com.grigowashere.aismap.utils.SettingsManager settingsManager =
|
||||
new com.grigowashere.aismap.utils.SettingsManager(context);
|
||||
|
||||
int staleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes();
|
||||
long thresholdEpochMs = System.currentTimeMillis() - (staleRemoveMinutes * 60 * 1000L);
|
||||
|
||||
repository.deleteStaleAIS(thresholdEpochMs);
|
||||
|
||||
Log.i(TAG, "Выполнена очистка БД от AIS целей старше " + staleRemoveMinutes + " минут");
|
||||
|
||||
// Планируем следующую очистку
|
||||
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
|
||||
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при очистке БД от устаревших AIS целей: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,6 +875,12 @@ public class AppController implements
|
||||
*/
|
||||
public void cleanup() {
|
||||
stopAllListeners();
|
||||
stopDatabaseCleanup();
|
||||
|
||||
// Очищаем Handler'ы для предотвращения утечек памяти
|
||||
if (uiHandler != null) {
|
||||
uiHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
if (udpListener != null) {
|
||||
udpListener.cleanup();
|
||||
@@ -595,11 +894,51 @@ public class AppController implements
|
||||
gpsLocationListener.cleanup();
|
||||
}
|
||||
|
||||
if (notificationService != null) {
|
||||
notificationService.cleanup();
|
||||
}
|
||||
|
||||
if (executor != null && !executor.isShutdown()) {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Метки времени последних сообщений ($ и !) =====
|
||||
private void updateLastMessageAgesFromRaw(String raw) {
|
||||
if (raw == null) return;
|
||||
long now = android.os.SystemClock.elapsedRealtime();
|
||||
String[] lines = raw.split("\r?\n");
|
||||
for (String line : lines) {
|
||||
if (line == null) continue;
|
||||
String t = line.trim();
|
||||
if (t.isEmpty()) continue;
|
||||
char c = t.charAt(0);
|
||||
if (c == '$') {
|
||||
lastGPSMessageRealtimeMs = now;
|
||||
break;
|
||||
} else if (c == '!') {
|
||||
lastAISMessageRealtimeMs = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Возвращает секунды с последнего GPS ($) сообщения; -1 если не было */
|
||||
public int getSecondsSinceLastGPSMessage() {
|
||||
if (lastGPSMessageRealtimeMs <= 0) return -1;
|
||||
long diff = android.os.SystemClock.elapsedRealtime() - lastGPSMessageRealtimeMs;
|
||||
if (diff < 0) return 0;
|
||||
return (int)(diff / 1000L);
|
||||
}
|
||||
|
||||
/** Возвращает секунды с последнего AIS (!) сообщения; -1 если не было */
|
||||
public int getSecondsSinceLastAISMessage() {
|
||||
if (lastAISMessageRealtimeMs <= 0) return -1;
|
||||
long diff = android.os.SystemClock.elapsedRealtime() - lastAISMessageRealtimeMs;
|
||||
if (diff < 0) return 0;
|
||||
return (int)(diff / 1000L);
|
||||
}
|
||||
|
||||
// Методы для управления настройками
|
||||
|
||||
/**
|
||||
@@ -706,4 +1045,142 @@ public class AppController implements
|
||||
dataMode != null ? dataMode : "не установлен"
|
||||
);
|
||||
}
|
||||
|
||||
// ===== Методы для работы с путем судна =====
|
||||
|
||||
/**
|
||||
* Получает контроллер пути судна
|
||||
*/
|
||||
public VesselPathController getPathController() {
|
||||
return pathController;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает информацию о пути судна
|
||||
*/
|
||||
public String getVesselPathInfo() {
|
||||
if (pathController != null) {
|
||||
return pathController.getPathInfo();
|
||||
}
|
||||
return "Контроллер пути не инициализирован";
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает путь судна
|
||||
*/
|
||||
public void clearVesselPath() {
|
||||
if (pathController != null) {
|
||||
pathController.clearPath();
|
||||
Log.i(TAG, "Путь судна очищен");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет путь судна
|
||||
*/
|
||||
public void saveVesselPath() {
|
||||
if (pathController != null) {
|
||||
Log.d(TAG, "Сохранение пути судна: " + pathController.getPathInfo());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет точку в путь AIS судна
|
||||
*/
|
||||
private void addAISVesselPathPoint(AISVessel vessel) {
|
||||
if (vessel == null || vessel.getMmsi() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем валидность координат
|
||||
if (!isValidCoordinates(vessel.getLatitude(), vessel.getLongitude())) {
|
||||
Log.d(TAG, "addAISVesselPathPoint: AIS vessel " + vessel.getMmsi() +
|
||||
" has invalid coordinates " + vessel.getLatitude() + "," + vessel.getLongitude() +
|
||||
" - skipping path point");
|
||||
return;
|
||||
}
|
||||
|
||||
String mmsi = vessel.getMmsi();
|
||||
|
||||
// Получаем или создаем VesselPathController для этого AIS судна
|
||||
VesselPathController aisPathController = aisPathControllers.get(mmsi);
|
||||
if (aisPathController == null) {
|
||||
// Ограничиваем количество трекеров для производительности
|
||||
if (aisPathControllers.size() >= 20) {
|
||||
Log.w(TAG, "Достигнуто максимальное количество AIS трекеров (20), пропускаем создание для " + mmsi);
|
||||
return;
|
||||
}
|
||||
|
||||
aisPathController = new VesselPathController(context, settingsManager, mmsi);
|
||||
aisPathControllers.put(mmsi, aisPathController);
|
||||
Log.d(TAG, "Создан VesselPathController для AIS судна " + mmsi + " (всего трекеров: " + aisPathControllers.size() + ")");
|
||||
}
|
||||
|
||||
// Добавляем точку в путь
|
||||
boolean pointAdded = aisPathController.addPathPoint(
|
||||
vessel.getLongitude(),
|
||||
vessel.getLatitude(),
|
||||
(float) vessel.getSpeed()
|
||||
);
|
||||
|
||||
if (pointAdded) {
|
||||
Log.d(TAG, "Точка пути добавлена для AIS " + mmsi + ": " + aisPathController.getPathPointsCount() + " точек");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет валидность координат
|
||||
* Игнорирует координаты 0,0 и 181,91 (невалидные значения AIS)
|
||||
*/
|
||||
private boolean isValidCoordinates(double latitude, double longitude) {
|
||||
// Проверяем на нулевые координаты
|
||||
if (latitude == 0.0 && longitude == 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем на невалидные координаты AIS (181, 91)
|
||||
if (latitude == 91.0 && longitude == 181.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем на стандартные границы координат
|
||||
if (latitude < -90.0 || latitude > 90.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (longitude < -180.0 || longitude > 180.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает VesselPathController для AIS судна
|
||||
*/
|
||||
public VesselPathController getAISVesselPathController(String mmsi) {
|
||||
return aisPathControllers.get(mmsi);
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает путь AIS судна
|
||||
*/
|
||||
public void clearAISVesselPath(String mmsi) {
|
||||
VesselPathController aisPathController = aisPathControllers.get(mmsi);
|
||||
if (aisPathController != null) {
|
||||
aisPathController.clearPath();
|
||||
Log.d(TAG, "Путь AIS судна " + mmsi + " очищен");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все пути AIS судов
|
||||
*/
|
||||
public void clearAllAISVesselPaths() {
|
||||
for (VesselPathController controller : aisPathControllers.values()) {
|
||||
controller.clearPath();
|
||||
}
|
||||
aisPathControllers.clear();
|
||||
Log.d(TAG, "Все пути AIS судов очищены");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +279,7 @@ public class GPSLocationListener implements LocationListener {
|
||||
this.pdop = pdop;
|
||||
this.hdop = hdop;
|
||||
this.vdop = vdop;
|
||||
Log.d(TAG, "📊 DOP обновлен: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop);
|
||||
// Убираем шумный лог DOP обновлений
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,8 +293,7 @@ public class GPSLocationListener implements LocationListener {
|
||||
// Устанавливаем только количество активных спутников
|
||||
vessel.setActiveSatellites(activeSatellites);
|
||||
|
||||
Log.d(TAG, "Обновлен Vessel: активных спутников=" + activeSatellites +
|
||||
" (общее количество из NMEA: " + vessel.getSatellites() + ")");
|
||||
// Убираем шумный лог обновления Vessel
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.content.Context;
|
||||
import android.util.Log;
|
||||
import com.grigowashere.aismap.maps.MapInterface;
|
||||
import com.grigowashere.aismap.maps.YandexMapImpl;
|
||||
import com.grigowashere.aismap.maps.MapLibreMapImpl;
|
||||
import com.yandex.mapkit.mapview.MapView;
|
||||
|
||||
/**
|
||||
@@ -18,6 +19,7 @@ public class MapController {
|
||||
private Context context;
|
||||
private MapInterface currentMapInterface;
|
||||
private MapView mapView;
|
||||
private org.maplibre.android.maps.MapView mapLibreView;
|
||||
|
||||
public MapController(Context context) {
|
||||
this.context = context;
|
||||
@@ -40,6 +42,41 @@ public class MapController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует MapLibre
|
||||
*/
|
||||
public MapInterface initializeMapLibre(org.maplibre.android.maps.MapView mapLibreView) {
|
||||
try {
|
||||
this.mapLibreView = mapLibreView;
|
||||
Log.i(TAG, "Создаем интерфейс для MapLibre");
|
||||
currentMapInterface = new MapLibreMapImpl(context, mapLibreView);
|
||||
return currentMapInterface;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при создании интерфейса MapLibre: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает VesselPathController в текущий интерфейс карты
|
||||
*/
|
||||
public void setVesselPathController(VesselPathController pathController) {
|
||||
if (currentMapInterface instanceof MapLibreMapImpl) {
|
||||
((MapLibreMapImpl) currentMapInterface).setVesselPathController(pathController);
|
||||
Log.i(TAG, "VesselPathController установлен в MapLibreMapImpl");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает AppController в текущий интерфейс карты
|
||||
*/
|
||||
public void setAppController(AppController appController) {
|
||||
if (currentMapInterface instanceof MapLibreMapImpl) {
|
||||
((MapLibreMapImpl) currentMapInterface).setAppController(appController);
|
||||
Log.i(TAG, "AppController установлен в MapLibreMapImpl");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует Яндекс.Карты
|
||||
*/
|
||||
@@ -81,9 +118,8 @@ public class MapController {
|
||||
* Запускает карту
|
||||
*/
|
||||
public void startMap() {
|
||||
if (mapView != null) {
|
||||
mapView.onStart();
|
||||
}
|
||||
if (mapView != null) { mapView.onStart(); }
|
||||
if (mapLibreView != null) { mapLibreView.onStart(); }
|
||||
|
||||
if (isYandexMapsInitialized) {
|
||||
com.yandex.mapkit.MapKitFactory.getInstance().onStart();
|
||||
@@ -94,9 +130,8 @@ public class MapController {
|
||||
* Останавливает карту
|
||||
*/
|
||||
public void stopMap() {
|
||||
if (mapView != null) {
|
||||
mapView.onStop();
|
||||
}
|
||||
if (mapView != null) { mapView.onStop(); }
|
||||
if (mapLibreView != null) { mapLibreView.onStop(); }
|
||||
|
||||
if (isYandexMapsInitialized) {
|
||||
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
|
||||
@@ -125,9 +160,8 @@ public class MapController {
|
||||
currentMapInterface.cleanup();
|
||||
}
|
||||
|
||||
if (mapView != null) {
|
||||
mapView.onStop();
|
||||
}
|
||||
if (mapView != null) { mapView.onStop(); }
|
||||
if (mapLibreView != null) { mapLibreView.onStop(); }
|
||||
|
||||
if (isYandexMapsInitialized) {
|
||||
com.yandex.mapkit.MapKitFactory.getInstance().onStop();
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
package com.grigowashere.aismap.controllers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.grigowashere.aismap.models.VesselPathPoint;
|
||||
import com.grigowashere.aismap.utils.SettingsManager;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Контроллер для управления путем судна
|
||||
* Отвечает за запись, сохранение и восстановление точек пути
|
||||
*/
|
||||
public class VesselPathController {
|
||||
|
||||
private static final String TAG = "VesselPathController";
|
||||
private static final String PREFS_NAME_PREFIX = "VesselPathData_";
|
||||
private static final String KEY_PATH_POINTS = "path_points";
|
||||
private static final String KEY_LAST_UPDATE = "last_update";
|
||||
|
||||
// Минимальное расстояние между точками (в метрах)
|
||||
private static final double MIN_DISTANCE_METERS = 1.0; // Уменьшено с 10.0 до 1.0 метра
|
||||
|
||||
// Минимальное время между точками (в секундах)
|
||||
private static final long MIN_TIME_SECONDS = 1; // Уменьшено с 5 до 1 секунды
|
||||
|
||||
// Максимальное количество точек для производительности
|
||||
private static final int MAX_PATH_POINTS = 1000; // Ограничение для предотвращения зависаний
|
||||
|
||||
private Context context;
|
||||
private SettingsManager settingsManager;
|
||||
private SharedPreferences prefs;
|
||||
private String vesselId; // Уникальный идентификатор судна
|
||||
|
||||
// Handler для UI операций
|
||||
private Handler uiHandler;
|
||||
|
||||
// Список точек пути
|
||||
private List<VesselPathPoint> pathPoints;
|
||||
|
||||
// Последняя добавленная точка
|
||||
private VesselPathPoint lastPoint;
|
||||
|
||||
public VesselPathController(Context context, SettingsManager settingsManager) {
|
||||
this(context, settingsManager, "own_vessel"); // По умолчанию для собственного судна
|
||||
}
|
||||
|
||||
public VesselPathController(Context context, SettingsManager settingsManager, String vesselId) {
|
||||
this.context = context;
|
||||
this.settingsManager = settingsManager;
|
||||
this.vesselId = vesselId;
|
||||
this.prefs = context.getSharedPreferences(PREFS_NAME_PREFIX + vesselId, Context.MODE_PRIVATE);
|
||||
this.pathPoints = new ArrayList<>();
|
||||
this.uiHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
// Загружаем сохраненные точки в фоновом потоке
|
||||
loadPathPointsAsync();
|
||||
|
||||
Log.i(TAG, "VesselPathController инициализирован для судна " + vesselId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет новую точку пути
|
||||
* @param longitude долгота
|
||||
* @param latitude широта
|
||||
* @param speed скорость в узлах
|
||||
* @return true если точка была добавлена, false если пропущена
|
||||
*/
|
||||
public boolean addPathPoint(double longitude, double latitude, float speed) {
|
||||
// Проверяем валидность координат
|
||||
if (!isValidCoordinates(latitude, longitude)) {
|
||||
Log.d(TAG, "addPathPoint: invalid coordinates " + latitude + "," + longitude + " - skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
VesselPathPoint newPoint = new VesselPathPoint(longitude, latitude, speed);
|
||||
|
||||
// Проверяем, нужно ли добавлять точку
|
||||
if (shouldAddPoint(newPoint)) {
|
||||
// Синхронизируем доступ к pathPoints для избежания гонки потоков
|
||||
synchronized (pathPoints) {
|
||||
pathPoints.add(newPoint);
|
||||
lastPoint = newPoint;
|
||||
|
||||
// Ограничиваем количество точек
|
||||
limitPathPoints();
|
||||
}
|
||||
|
||||
// Сохраняем изменения
|
||||
savePathPoints();
|
||||
|
||||
Log.d(TAG, "Добавлена точка пути: " + newPoint + ", всего точек: " + pathPoints.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли добавлять точку
|
||||
*/
|
||||
private boolean shouldAddPoint(VesselPathPoint newPoint) {
|
||||
// Если это первая точка
|
||||
if (lastPoint == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Проверяем расстояние
|
||||
double distance = lastPoint.distanceTo(newPoint);
|
||||
if (distance < MIN_DISTANCE_METERS) {
|
||||
Log.d(TAG, "Точка пропущена: расстояние слишком мало (" + distance + "м)");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем время
|
||||
long timeDiff = lastPoint.timeDifferenceSeconds(newPoint);
|
||||
if (timeDiff < MIN_TIME_SECONDS) {
|
||||
Log.d(TAG, "Точка пропущена: время слишком мало (" + timeDiff + "с)");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ограничивает количество точек пути
|
||||
*/
|
||||
private void limitPathPoints() {
|
||||
int maxPoints = settingsManager.getPathMaxPoints();
|
||||
Log.d(TAG, "limitPathPoints: текущих точек=" + pathPoints.size() + ", лимит=" + maxPoints);
|
||||
|
||||
if (pathPoints.size() > maxPoints) {
|
||||
// Удаляем самые старые точки
|
||||
int toRemove = pathPoints.size() - maxPoints;
|
||||
for (int i = 0; i < toRemove; i++) {
|
||||
pathPoints.remove(0);
|
||||
}
|
||||
Log.d(TAG, "Удалено " + toRemove + " старых точек пути, осталось: " + pathPoints.size());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает точки пути в формате JSONArray для MapLibre
|
||||
*/
|
||||
public JSONArray getPathCoordinates() {
|
||||
JSONArray coords = new JSONArray();
|
||||
|
||||
try {
|
||||
// Синхронизируем доступ к pathPoints для избежания гонки потоков
|
||||
synchronized (pathPoints) {
|
||||
for (VesselPathPoint point : pathPoints) {
|
||||
JSONArray coord = new JSONArray();
|
||||
coord.put(point.getLongitude());
|
||||
coord.put(point.getLatitude());
|
||||
coords.put(coord);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Ошибка создания координат пути", e);
|
||||
}
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает точки пути в формате JSONArray для MapLibre с учетом скорости
|
||||
* Возвращает массив координат с информацией о скорости для динамического пунктира
|
||||
*/
|
||||
public JSONArray getPathCoordinatesWithSpeed() {
|
||||
JSONArray coords = new JSONArray();
|
||||
|
||||
try {
|
||||
// Синхронизируем доступ к pathPoints для избежания гонки потоков
|
||||
synchronized (pathPoints) {
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "getPathCoordinatesWithSpeed: обрабатываем " + pathPoints.size() + " точек");
|
||||
for (VesselPathPoint point : pathPoints) {
|
||||
JSONArray coord = new JSONArray();
|
||||
coord.put(point.getLongitude());
|
||||
coord.put(point.getLatitude());
|
||||
coord.put(point.getSpeed()); // Добавляем скорость для динамического пунктира
|
||||
coords.put(coord);
|
||||
}
|
||||
}
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "getPathCoordinatesWithSpeed: создано " + coords.length() + " координат");
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Ошибка создания координат пути с скоростью", e);
|
||||
}
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет среднюю скорость на участке пути
|
||||
*/
|
||||
public float getAverageSpeed() {
|
||||
if (pathPoints.size() < 2) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float totalSpeed = 0.0f;
|
||||
for (VesselPathPoint point : pathPoints) {
|
||||
totalSpeed += point.getSpeed();
|
||||
}
|
||||
|
||||
return totalSpeed / pathPoints.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает последнюю точку пути
|
||||
*/
|
||||
public VesselPathPoint getLastPoint() {
|
||||
return lastPoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает количество точек пути
|
||||
*/
|
||||
public int getPathPointsCount() {
|
||||
synchronized (pathPoints) {
|
||||
return pathPoints.size();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все точки пути
|
||||
*/
|
||||
public void clearPath() {
|
||||
Log.i(TAG, "clearPath() вызван");
|
||||
|
||||
synchronized (pathPoints) {
|
||||
int pointsCount = pathPoints.size();
|
||||
pathPoints.clear();
|
||||
lastPoint = null;
|
||||
Log.i(TAG, "Очищено " + pointsCount + " точек из памяти");
|
||||
}
|
||||
|
||||
// Удаляем точки из SharedPreferences
|
||||
prefs.edit()
|
||||
.remove(KEY_PATH_POINTS)
|
||||
.putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
|
||||
.apply();
|
||||
Log.i(TAG, "Путь очищен из памяти и SharedPreferences");
|
||||
}
|
||||
|
||||
/**
|
||||
* Сохраняет точки пути в SharedPreferences
|
||||
*/
|
||||
private void savePathPoints() {
|
||||
try {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
|
||||
// Синхронизируем доступ к pathPoints для избежания гонки потоков
|
||||
synchronized (pathPoints) {
|
||||
for (VesselPathPoint point : pathPoints) {
|
||||
JSONObject jsonPoint = new JSONObject();
|
||||
jsonPoint.put("longitude", point.getLongitude());
|
||||
jsonPoint.put("latitude", point.getLatitude());
|
||||
jsonPoint.put("speed", point.getSpeed());
|
||||
jsonPoint.put("timestamp", point.getTimestamp());
|
||||
jsonArray.put(jsonPoint);
|
||||
}
|
||||
}
|
||||
|
||||
prefs.edit()
|
||||
.putString(KEY_PATH_POINTS, jsonArray.toString())
|
||||
.putLong(KEY_LAST_UPDATE, System.currentTimeMillis())
|
||||
.apply();
|
||||
|
||||
Log.d(TAG, "Сохранено " + pathPoints.size() + " точек пути");
|
||||
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Ошибка сохранения точек пути", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает точки пути из SharedPreferences в фоновом потоке
|
||||
*/
|
||||
private void loadPathPointsAsync() {
|
||||
new Thread(() -> {
|
||||
try {
|
||||
String jsonString = prefs.getString(KEY_PATH_POINTS, null);
|
||||
if (jsonString == null || jsonString.isEmpty()) {
|
||||
Log.d(TAG, "Нет сохраненных точек пути");
|
||||
return;
|
||||
}
|
||||
|
||||
JSONArray jsonArray = new JSONArray(jsonString);
|
||||
List<VesselPathPoint> loadedPoints = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonPoint = jsonArray.getJSONObject(i);
|
||||
VesselPathPoint point = new VesselPathPoint(
|
||||
jsonPoint.getDouble("longitude"),
|
||||
jsonPoint.getDouble("latitude"),
|
||||
(float) jsonPoint.getDouble("speed"),
|
||||
jsonPoint.getLong("timestamp")
|
||||
);
|
||||
loadedPoints.add(point);
|
||||
}
|
||||
|
||||
// Обновляем UI поток через существующий Handler
|
||||
uiHandler.post(() -> {
|
||||
synchronized (pathPoints) {
|
||||
pathPoints.clear();
|
||||
pathPoints.addAll(loadedPoints);
|
||||
|
||||
// Устанавливаем последнюю точку
|
||||
if (!pathPoints.isEmpty()) {
|
||||
lastPoint = pathPoints.get(pathPoints.size() - 1);
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Загружено " + loadedPoints.size() + " точек пути для судна " + vesselId);
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка загрузки точек пути", e);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает точки пути из SharedPreferences (синхронно)
|
||||
*/
|
||||
private void loadPathPoints() {
|
||||
try {
|
||||
String jsonString = prefs.getString(KEY_PATH_POINTS, null);
|
||||
if (jsonString == null || jsonString.isEmpty()) {
|
||||
Log.d(TAG, "Нет сохраненных точек пути");
|
||||
return;
|
||||
}
|
||||
|
||||
JSONArray jsonArray = new JSONArray(jsonString);
|
||||
pathPoints.clear();
|
||||
|
||||
for (int i = 0; i < jsonArray.length(); i++) {
|
||||
JSONObject jsonPoint = jsonArray.getJSONObject(i);
|
||||
VesselPathPoint point = new VesselPathPoint(
|
||||
jsonPoint.getDouble("longitude"),
|
||||
jsonPoint.getDouble("latitude"),
|
||||
(float) jsonPoint.getDouble("speed"),
|
||||
jsonPoint.getLong("timestamp")
|
||||
);
|
||||
pathPoints.add(point);
|
||||
}
|
||||
|
||||
// Устанавливаем последнюю точку
|
||||
if (!pathPoints.isEmpty()) {
|
||||
lastPoint = pathPoints.get(pathPoints.size() - 1);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Загружено " + pathPoints.size() + " точек пути");
|
||||
|
||||
} catch (JSONException e) {
|
||||
Log.e(TAG, "Ошибка загрузки точек пути", e);
|
||||
pathPoints.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает информацию о пути для отладки
|
||||
*/
|
||||
public String getPathInfo() {
|
||||
synchronized (pathPoints) {
|
||||
if (pathPoints.isEmpty()) {
|
||||
return "Путь пуст";
|
||||
}
|
||||
|
||||
VesselPathPoint first = pathPoints.get(0);
|
||||
VesselPathPoint last = pathPoints.get(pathPoints.size() - 1);
|
||||
|
||||
return String.format("Путь: %d точек, от %s до %s, средняя скорость: %.1f узлов",
|
||||
pathPoints.size(),
|
||||
first.toString(),
|
||||
last.toString(),
|
||||
getAverageSpeed());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет валидность координат
|
||||
* Игнорирует координаты 0,0 и 181,91 (невалидные значения AIS)
|
||||
*/
|
||||
private boolean isValidCoordinates(double latitude, double longitude) {
|
||||
// Проверяем на нулевые координаты
|
||||
if (latitude == 0.0 && longitude == 0.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем на невалидные координаты AIS (181, 91)
|
||||
if (latitude == 91.0 && longitude == 181.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем на стандартные границы координат
|
||||
if (latitude < -90.0 || latitude > 90.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (longitude < -180.0 || longitude > 180.0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка ресурсов
|
||||
*/
|
||||
public void cleanup() {
|
||||
if (uiHandler != null) {
|
||||
uiHandler.removeCallbacksAndMessages(null);
|
||||
uiHandler = null;
|
||||
}
|
||||
Log.d(TAG, "VesselPathController очищен для судна " + vesselId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.grigowashere.aismap.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
|
||||
import com.grigowashere.aismap.data.dao.AISVesselDao;
|
||||
import com.grigowashere.aismap.data.dao.VesselDao;
|
||||
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||
|
||||
@Database(entities = {AISVesselEntity.class, VesselEntity.class}, version = 3, exportSchema = false)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
public abstract AISVesselDao aisVesselDao();
|
||||
public abstract VesselDao vesselDao();
|
||||
|
||||
private static volatile AppDatabase INSTANCE;
|
||||
|
||||
public static AppDatabase getInstance(Context context) {
|
||||
if (INSTANCE == null) {
|
||||
synchronized (AppDatabase.class) {
|
||||
if (INSTANCE == null) {
|
||||
INSTANCE = Room.databaseBuilder(
|
||||
context.getApplicationContext(),
|
||||
AppDatabase.class,
|
||||
"aismap.db"
|
||||
).fallbackToDestructiveMigration().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.grigowashere.aismap.data;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.grigowashere.aismap.data.dao.AISVesselDao;
|
||||
import com.grigowashere.aismap.data.dao.VesselDao;
|
||||
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
public class Repository {
|
||||
private final AISVesselDao aisVesselDao;
|
||||
private final VesselDao vesselDao;
|
||||
private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
public Repository(Context context) {
|
||||
AppDatabase db = AppDatabase.getInstance(context);
|
||||
this.aisVesselDao = db.aisVesselDao();
|
||||
this.vesselDao = db.vesselDao();
|
||||
}
|
||||
|
||||
public void upsertAIS(AISVesselEntity entity) {
|
||||
ioExecutor.execute(() -> aisVesselDao.upsert(entity));
|
||||
}
|
||||
|
||||
public void deleteStaleAIS(long thresholdEpochMs) {
|
||||
ioExecutor.execute(() -> aisVesselDao.deleteStale(thresholdEpochMs));
|
||||
}
|
||||
|
||||
public List<AISVesselEntity> getAllAISSync() {
|
||||
return aisVesselDao.getAll();
|
||||
}
|
||||
|
||||
public LiveData<List<AISVesselEntity>> observeAllAIS() {
|
||||
return aisVesselDao.observeAll();
|
||||
}
|
||||
|
||||
public AISVesselEntity getAISByMmsiSync(String mmsi) {
|
||||
return aisVesselDao.getByMmsi(mmsi);
|
||||
}
|
||||
|
||||
public void upsertOwnVessel(VesselEntity entity) {
|
||||
ioExecutor.execute(() -> {
|
||||
vesselDao.upsert(entity);
|
||||
});
|
||||
}
|
||||
|
||||
public VesselEntity getLatestOwnVesselSync() {
|
||||
return vesselDao.getLatest();
|
||||
}
|
||||
|
||||
public void getLatestOwnVesselAsync(RepositoryCallback<VesselEntity> callback) {
|
||||
ioExecutor.execute(() -> {
|
||||
try {
|
||||
VesselEntity entity = vesselDao.getLatest();
|
||||
callback.onComplete(entity);
|
||||
} catch (Exception e) {
|
||||
callback.onError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public interface RepositoryCallback<T> {
|
||||
void onComplete(T result);
|
||||
void onError(Exception e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.grigowashere.aismap.data.dao;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface AISVesselDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
void upsert(AISVesselEntity entity);
|
||||
|
||||
@Update
|
||||
void update(AISVesselEntity entity);
|
||||
|
||||
@Query("SELECT * FROM ais_vessels")
|
||||
List<AISVesselEntity> getAll();
|
||||
|
||||
@Query("SELECT * FROM ais_vessels")
|
||||
LiveData<List<AISVesselEntity>> observeAll();
|
||||
|
||||
@Query("SELECT * FROM ais_vessels WHERE mmsi = :mmsi LIMIT 1")
|
||||
AISVesselEntity getByMmsi(String mmsi);
|
||||
|
||||
@Query("DELETE FROM ais_vessels WHERE lastUpdateEpochMs < :threshold")
|
||||
void deleteStale(long threshold);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.grigowashere.aismap.data.dao;
|
||||
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Update;
|
||||
|
||||
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||
|
||||
@Dao
|
||||
public interface VesselDao {
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
long upsert(VesselEntity entity);
|
||||
|
||||
@Update
|
||||
void update(VesselEntity entity);
|
||||
|
||||
@Query("SELECT * FROM own_vessel ORDER BY id DESC LIMIT 1")
|
||||
VesselEntity getLatest();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.grigowashere.aismap.data.entity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
/**
|
||||
* Room-сущность для хранения AIS цели
|
||||
* Теперь содержит ВСЕ поля из AISVessel модели
|
||||
*/
|
||||
@Entity(tableName = "ais_vessels")
|
||||
public class AISVesselEntity {
|
||||
@PrimaryKey
|
||||
@NonNull
|
||||
public String mmsi;
|
||||
|
||||
// Основная информация о судне
|
||||
public String vesselName;
|
||||
public String callSign;
|
||||
public int imo; // IMO номер
|
||||
public String vesselType; // тип судна
|
||||
|
||||
// Позиция и движение
|
||||
public double latitude;
|
||||
public double longitude;
|
||||
public double course; // курс в градусах (0-360)
|
||||
public double speed; // скорость в узлах
|
||||
public double heading; // направление движения в градусах
|
||||
public double rateOfTurn; // скорость поворота в градусах/минуту
|
||||
|
||||
// Размеры судна
|
||||
public double length; // длина судна в метрах
|
||||
public double width; // ширина судна в метрах
|
||||
public double draft; // осадка в метрах
|
||||
|
||||
// Навигационная информация
|
||||
public String destination; // пункт назначения
|
||||
public long etaEpochMs; // предполагаемое время прибытия (epoch ms)
|
||||
public String navigationalStatus; // навигационный статус
|
||||
public boolean positionAccuracy; // точность позиции
|
||||
|
||||
// Техническая информация
|
||||
public int signalStrength; // сила AIS сигнала
|
||||
public String vesselClass; // класс судна (Class A, Class B, Extended Class B)
|
||||
public String vendorId; // идентификатор производителя оборудования
|
||||
public String lastSafetyMessage; // последнее сообщение безопасности
|
||||
|
||||
// Состояние и время
|
||||
public long lastUpdateEpochMs; // время последнего обновления (epoch ms)
|
||||
public boolean isActive; // активно ли судно
|
||||
public boolean selected; // выделено ли судно на карте
|
||||
|
||||
public AISVesselEntity(@NonNull String mmsi) {
|
||||
this.mmsi = mmsi;
|
||||
this.isActive = true;
|
||||
this.selected = false;
|
||||
this.lastUpdateEpochMs = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.grigowashere.aismap.data.entity;
|
||||
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
/**
|
||||
* Room-сущность для хранения нашего судна/позиции
|
||||
*/
|
||||
@Entity(tableName = "own_vessel")
|
||||
public class VesselEntity {
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
public long id;
|
||||
|
||||
public double latitude;
|
||||
public double longitude;
|
||||
public double course;
|
||||
public double speed;
|
||||
public double heading;
|
||||
public float accuracy;
|
||||
public long fixTime;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.grigowashere.aismap.data.mapper;
|
||||
|
||||
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/**
|
||||
* Маппер для конвертации между AISVessel (модель) и AISVesselEntity (БД)
|
||||
* Решает проблему потери данных при сохранении/восстановлении AIS судов
|
||||
*/
|
||||
public class AISVesselMapper {
|
||||
|
||||
/**
|
||||
* Конвертирует AISVessel модель в AISVesselEntity для сохранения в БД
|
||||
*/
|
||||
public static AISVesselEntity toEntity(AISVessel vessel) {
|
||||
if (vessel == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AISVesselEntity entity = new AISVesselEntity(vessel.getMmsi());
|
||||
|
||||
// Основная информация о судне
|
||||
entity.vesselName = vessel.getVesselName();
|
||||
entity.callSign = vessel.getCallSign();
|
||||
entity.imo = vessel.getImo();
|
||||
entity.vesselType = vessel.getVesselType();
|
||||
|
||||
// Позиция и движение
|
||||
entity.latitude = vessel.getLatitude();
|
||||
entity.longitude = vessel.getLongitude();
|
||||
entity.course = vessel.getCourse();
|
||||
entity.speed = vessel.getSpeed();
|
||||
entity.heading = vessel.getHeading();
|
||||
|
||||
// Размеры судна
|
||||
entity.length = vessel.getLength();
|
||||
entity.width = vessel.getWidth();
|
||||
entity.draft = vessel.getDraft();
|
||||
|
||||
// Навигационная информация
|
||||
entity.destination = vessel.getDestination();
|
||||
entity.etaEpochMs = convertLocalDateTimeToEpochMs(vessel.getEta());
|
||||
entity.navigationalStatus = vessel.getNavigationalStatus();
|
||||
entity.positionAccuracy = vessel.isPositionAccuracy();
|
||||
|
||||
// Техническая информация
|
||||
entity.signalStrength = vessel.getSignalStrength();
|
||||
entity.vesselClass = vessel.getVesselClass();
|
||||
entity.vendorId = vessel.getVendorId();
|
||||
entity.lastSafetyMessage = vessel.getLastSafetyMessage();
|
||||
|
||||
// Состояние и время
|
||||
entity.lastUpdateEpochMs = convertLocalDateTimeToEpochMs(vessel.getLastUpdate());
|
||||
entity.isActive = vessel.isActive();
|
||||
entity.selected = vessel.isSelected();
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует AISVesselEntity из БД в AISVessel модель
|
||||
*/
|
||||
public static AISVessel toModel(AISVesselEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
AISVessel vessel = new AISVessel(entity.mmsi);
|
||||
|
||||
// Основная информация о судне
|
||||
vessel.setVesselName(entity.vesselName);
|
||||
vessel.setCallSign(entity.callSign);
|
||||
vessel.setImo(entity.imo);
|
||||
vessel.setVesselType(entity.vesselType);
|
||||
|
||||
// Позиция и движение
|
||||
vessel.setLatitude(entity.latitude);
|
||||
vessel.setLongitude(entity.longitude);
|
||||
vessel.setCourse(entity.course);
|
||||
vessel.setSpeed(entity.speed);
|
||||
vessel.setHeading(entity.heading);
|
||||
|
||||
// Размеры судна
|
||||
vessel.setLength(entity.length);
|
||||
vessel.setWidth(entity.width);
|
||||
vessel.setDraft(entity.draft);
|
||||
|
||||
// Навигационная информация
|
||||
vessel.setDestination(entity.destination);
|
||||
vessel.setEta(convertEpochMsToLocalDateTime(entity.etaEpochMs));
|
||||
vessel.setNavigationalStatus(entity.navigationalStatus);
|
||||
vessel.setPositionAccuracy(entity.positionAccuracy);
|
||||
|
||||
// Техническая информация
|
||||
vessel.setSignalStrength(entity.signalStrength);
|
||||
vessel.setVesselClass(entity.vesselClass);
|
||||
vessel.setVendorId(entity.vendorId);
|
||||
vessel.setLastSafetyMessage(entity.lastSafetyMessage);
|
||||
|
||||
// Состояние и время
|
||||
vessel.setLastUpdate(convertEpochMsToLocalDateTime(entity.lastUpdateEpochMs));
|
||||
vessel.setActive(entity.isActive);
|
||||
vessel.setSelected(entity.selected);
|
||||
|
||||
return vessel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует LocalDateTime в epoch milliseconds
|
||||
*/
|
||||
private static long convertLocalDateTimeToEpochMs(LocalDateTime dateTime) {
|
||||
if (dateTime == null) {
|
||||
return 0;
|
||||
}
|
||||
return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует epoch milliseconds в LocalDateTime
|
||||
*/
|
||||
private static LocalDateTime convertEpochMsToLocalDateTime(long epochMs) {
|
||||
if (epochMs <= 0) {
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMs), ZoneId.systemDefault());
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,15 @@ import android.graphics.Color;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.view.CursorOverlay;
|
||||
import com.grigowashere.aismap.R;
|
||||
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 android.view.ViewGroup;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -27,17 +30,30 @@ public class MapForgeImpl implements MapInterface {
|
||||
|
||||
private Map<String, Marker> aisMarkers;
|
||||
private Marker ownVesselMarker;
|
||||
private CursorOverlay cursorOverlay;
|
||||
private Vessel ownVessel;
|
||||
|
||||
public MapForgeImpl(Context context, MapView mapView) {
|
||||
this.context = context;
|
||||
this.mapView = mapView;
|
||||
this.aisMarkers = new HashMap<>();
|
||||
this.layers = mapView.getLayerManager().getLayers();
|
||||
this.cursorOverlay = new CursorOverlay(context);
|
||||
|
||||
// Добавляем overlay курсора в MapView
|
||||
if (mapView instanceof ViewGroup) {
|
||||
ViewGroup parent = (ViewGroup) mapView;
|
||||
// Проверяем, не добавлен ли уже курсор
|
||||
if (parent.findViewById(R.id.cursor_cross) == null) {
|
||||
parent.addView(cursorOverlay.getView());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// MapForge уже инициализирован
|
||||
setupMapMovementListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -53,6 +69,9 @@ public class MapForgeImpl implements MapInterface {
|
||||
layers.remove(ownVesselMarker);
|
||||
}
|
||||
|
||||
this.ownVessel = vessel;
|
||||
cursorOverlay.setOwnVessel(vessel);
|
||||
|
||||
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.BLUE, vessel.getCourse());
|
||||
|
||||
@@ -71,12 +90,17 @@ public class MapForgeImpl implements MapInterface {
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.BLUE, vessel.getCourse());
|
||||
ownVesselMarker.setBitmap(icon);
|
||||
}
|
||||
|
||||
this.ownVessel = vessel;
|
||||
cursorOverlay.setOwnVessel(vessel);
|
||||
}
|
||||
|
||||
@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());
|
||||
// Используем heading вместо course для поворота маркера AIS судна
|
||||
double rotationAngle = vessel.getHeading() > 0 ? vessel.getHeading() : vessel.getCourse();
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, rotationAngle);
|
||||
|
||||
Marker marker = new Marker(position, icon, 0, 0);
|
||||
// MapForge не поддерживает OnTapListener напрямую
|
||||
@@ -92,7 +116,9 @@ public class MapForgeImpl implements MapInterface {
|
||||
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||
marker.setLatLong(newPosition);
|
||||
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
|
||||
// Используем heading вместо course для поворота маркера AIS судна
|
||||
double rotationAngle = vessel.getHeading() > 0 ? vessel.getHeading() : vessel.getCourse();
|
||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, rotationAngle);
|
||||
marker.setBitmap(icon);
|
||||
}
|
||||
}
|
||||
@@ -144,6 +170,13 @@ public class MapForgeImpl implements MapInterface {
|
||||
this.markerClickListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearVesselPath() {
|
||||
// MapForge не поддерживает трекинг пути в данной реализации
|
||||
// Метод добавлен для совместимости с интерфейсом
|
||||
// В будущем можно добавить поддержку трекинга пути для MapForge
|
||||
}
|
||||
|
||||
private org.mapsforge.core.graphics.Bitmap createMapForgeIcon(int color, double course) {
|
||||
// Создаем простую иконку для MapForge
|
||||
// В реальном приложении нужно конвертировать Android Bitmap в MapForge Bitmap
|
||||
@@ -151,6 +184,65 @@ public class MapForgeImpl implements MapInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showCursor() {
|
||||
if (cursorOverlay != null) {
|
||||
// cursorOverlay.showCoordinates();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideCursor() {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.hideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCursorCoordinates(double latitude, double longitude) {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.updateCursorCoordinates(latitude, longitude);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCursorFromMapCenter() {
|
||||
if (cursorOverlay != null && mapView != null) {
|
||||
// Получаем координаты центра карты
|
||||
LatLong center = mapView.getModel().mapViewPosition.getCenter();
|
||||
cursorOverlay.updateCursorCoordinates(center.latitude, center.longitude);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает слушатель движения карты для обновления курсора
|
||||
*/
|
||||
private void setupMapMovementListener() {
|
||||
if (mapView != null) {
|
||||
// mapView.getModel().mapViewPosition.addObserver(new org.mapsforge.map.model.Observer() {
|
||||
// @Override
|
||||
// public void onChange() {
|
||||
// // Обновляем координаты курсора при движении карты
|
||||
// updateCursorFromMapCenter();
|
||||
// }
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAisVesselInfo(com.grigowashere.aismap.models.AISVessel vessel) {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.setAisVesselInfo(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAisVesselInfo() {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.clearAisVesselInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public MapView getMapView() {
|
||||
return mapView;
|
||||
}
|
||||
|
||||
@@ -79,6 +79,41 @@ public interface MapInterface {
|
||||
*/
|
||||
void setMarkerClickListener(MarkerClickListener listener);
|
||||
|
||||
/**
|
||||
* Очистка трекера пути собственного судна
|
||||
*/
|
||||
void clearVesselPath();
|
||||
|
||||
/**
|
||||
* Показать курсор на карте
|
||||
*/
|
||||
void showCursor();
|
||||
|
||||
/**
|
||||
* Скрыть курсор на карте
|
||||
*/
|
||||
void hideCursor();
|
||||
|
||||
/**
|
||||
* Обновить координаты курсора (центра экрана)
|
||||
*/
|
||||
void updateCursorCoordinates(double latitude, double longitude);
|
||||
|
||||
/**
|
||||
* Обновить координаты курсора автоматически при движении карты
|
||||
*/
|
||||
void updateCursorFromMapCenter();
|
||||
|
||||
/**
|
||||
* Установить информацию об AIS судне под курсором
|
||||
*/
|
||||
void setAisVesselInfo(com.grigowashere.aismap.models.AISVessel vessel);
|
||||
|
||||
/**
|
||||
* Очистить информацию об AIS судне
|
||||
*/
|
||||
void clearAisVesselInfo();
|
||||
|
||||
/**
|
||||
* Интерфейс для обработки кликов по меткам
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
/**
|
||||
* Интерфейс для управления маркерами на карте
|
||||
* Отделяет логику управления маркерами от конкретной реализации карты
|
||||
*/
|
||||
public interface MarkerManager {
|
||||
|
||||
/**
|
||||
* Инициализация менеджера маркеров
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* Очистка ресурсов менеджера маркеров
|
||||
*/
|
||||
void cleanup();
|
||||
|
||||
/**
|
||||
* Добавление или обновление маркера нашего судна
|
||||
*/
|
||||
void updateOwnVesselMarker(Vessel vessel);
|
||||
|
||||
/**
|
||||
* Добавление или обновление маркера AIS судна
|
||||
*/
|
||||
void updateAISVesselMarker(AISVessel vessel);
|
||||
|
||||
/**
|
||||
* Удаление маркера AIS судна
|
||||
*/
|
||||
void removeAISVesselMarker(String mmsi);
|
||||
|
||||
/**
|
||||
* Очистка всех AIS маркеров
|
||||
*/
|
||||
void clearAISVesselMarkers();
|
||||
|
||||
/**
|
||||
* Установка обработчика кликов по маркерам
|
||||
*/
|
||||
void setMarkerClickListener(MapInterface.MarkerClickListener listener);
|
||||
|
||||
/**
|
||||
* Обновление всех маркеров (например, при повороте карты)
|
||||
*/
|
||||
void refreshAllMarkers();
|
||||
|
||||
/**
|
||||
* Проверка и восстановление финализированных маркеров
|
||||
*/
|
||||
void checkAndRestoreMarkers();
|
||||
|
||||
/**
|
||||
* Получение количества активных маркеров
|
||||
*/
|
||||
int getActiveMarkerCount();
|
||||
|
||||
/**
|
||||
* Включает/выключает отображение путей движения
|
||||
*/
|
||||
void setPathTrackingEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Очищает путь конкретного судна
|
||||
*/
|
||||
void clearVesselPath(String mmsi);
|
||||
|
||||
/**
|
||||
* Очищает все пути движения
|
||||
*/
|
||||
void clearAllPaths();
|
||||
|
||||
/**
|
||||
* Обновляет настройки отображения путей
|
||||
*/
|
||||
void updatePathSettings(int pathColor, int predictionColor, float pathWidth, float predictionWidth);
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
/**
|
||||
* Обертка для маркера с управлением жизненным циклом
|
||||
* Предотвращает финализацию объектов и обеспечивает стабильную работу
|
||||
*/
|
||||
public abstract class MarkerWrapper {
|
||||
|
||||
protected String id;
|
||||
protected boolean isActive;
|
||||
protected long lastUpdateTime;
|
||||
protected long creationTime;
|
||||
|
||||
// Константы для управления жизненным циклом
|
||||
private static final long MARKER_LIFETIME = 5000; // 5 секунд
|
||||
private static final long UPDATE_THROTTLE = 200; // 0.2 секунды между обновлениями
|
||||
|
||||
public MarkerWrapper(String id) {
|
||||
this.id = id;
|
||||
this.isActive = true;
|
||||
this.creationTime = System.currentTimeMillis();
|
||||
this.lastUpdateTime = creationTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли обновлять маркер
|
||||
*/
|
||||
public boolean shouldUpdate() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
return (currentTime - lastUpdateTime) >= UPDATE_THROTTLE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, не устарел ли маркер
|
||||
*/
|
||||
public boolean isExpired() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
return (currentTime - creationTime) >= MARKER_LIFETIME;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет время последнего обновления
|
||||
*/
|
||||
public void markUpdated() {
|
||||
this.lastUpdateTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, активен ли маркер
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return isActive && !isExpired();
|
||||
}
|
||||
|
||||
/**
|
||||
* Деактивирует маркер
|
||||
*/
|
||||
public void deactivate() {
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает ID маркера
|
||||
*/
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Абстрактный метод для проверки состояния маркера
|
||||
*/
|
||||
public abstract boolean isValid();
|
||||
|
||||
/**
|
||||
* Абстрактный метод для обновления позиции маркера
|
||||
*/
|
||||
public abstract void updatePosition(double latitude, double longitude);
|
||||
|
||||
/**
|
||||
* Абстрактный метод для обновления курса маркера
|
||||
*/
|
||||
public abstract void updateCourse(double course);
|
||||
|
||||
/**
|
||||
* Абстрактный метод для удаления маркера
|
||||
*/
|
||||
public abstract void remove();
|
||||
|
||||
/**
|
||||
* Абстрактный метод для обновления иконки маркера
|
||||
*/
|
||||
public abstract void updateIcon();
|
||||
|
||||
/**
|
||||
* Абстрактный метод для установки обработчика кликов
|
||||
*/
|
||||
public abstract void setClickListener(Runnable clickHandler);
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.util.Log;
|
||||
import com.yandex.mapkit.geometry.Point;
|
||||
import com.yandex.mapkit.map.MapObjectCollection;
|
||||
import com.yandex.mapkit.map.PolylineMapObject;
|
||||
import com.yandex.mapkit.map.MapObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
/**
|
||||
* Класс для отслеживания и отображения пути движения судна
|
||||
* Отображает сплошную линию пройденного пути и прогнозируемое движение
|
||||
*/
|
||||
public class VesselPathTracker {
|
||||
|
||||
private static final String TAG = "VesselPathTracker";
|
||||
private static final int MAX_PATH_POINTS = 100; // Максимальное количество точек в пути
|
||||
private static final long MIN_TIME_BETWEEN_POINTS = 1000; // Минимальное время между точками (1 секунда)
|
||||
private static final double MIN_DISTANCE_BETWEEN_POINTS = 10.0; // Минимальное расстояние между точками (10 метров)
|
||||
|
||||
private String vesselId;
|
||||
private MapObjectCollection mapObjects;
|
||||
private ConcurrentLinkedQueue<PathPoint> pathHistory;
|
||||
private PolylineMapObject pathLine;
|
||||
private PolylineMapObject predictionLine;
|
||||
private long lastUpdateTime;
|
||||
private Point lastPosition;
|
||||
|
||||
|
||||
// Настройки отображения
|
||||
private int pathColor = Color.CYAN;
|
||||
private int predictionColor = Color.YELLOW;
|
||||
private float pathWidth = 3.0f;
|
||||
private float predictionWidth = 2.0f;
|
||||
private boolean isEnabled = true;
|
||||
|
||||
/**
|
||||
* Точка пути с временной меткой
|
||||
*/
|
||||
private static class PathPoint {
|
||||
public final Point position;
|
||||
public final long timestamp;
|
||||
public final double speed;
|
||||
public final double course;
|
||||
|
||||
public PathPoint(Point position, long timestamp, double speed, double course) {
|
||||
this.position = position;
|
||||
this.timestamp = timestamp;
|
||||
this.speed = speed;
|
||||
this.course = course;
|
||||
}
|
||||
}
|
||||
|
||||
public VesselPathTracker(String vesselId, MapObjectCollection mapObjects) {
|
||||
this.vesselId = vesselId;
|
||||
this.mapObjects = mapObjects;
|
||||
this.pathHistory = new ConcurrentLinkedQueue<>();
|
||||
this.lastUpdateTime = 0;
|
||||
Log.d(TAG, "Created VesselPathTracker for vessel: " + vesselId + ", mapObjects: " + (mapObjects != null ? "not null" : "null"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет путь судна новой позицией
|
||||
*/
|
||||
public void updatePosition(double latitude, double longitude, double speed, double course) {
|
||||
if (!isEnabled) {
|
||||
Log.d(TAG, "VesselPathTracker disabled for vessel: " + vesselId);
|
||||
return;
|
||||
}
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
Point newPosition = new Point(latitude, longitude);
|
||||
|
||||
Log.d(TAG, "updatePosition called for vessel: " + vesselId +
|
||||
", lat: " + latitude + ", lon: " + longitude +
|
||||
", speed: " + speed + ", course: " + course);
|
||||
|
||||
// Проверяем, нужно ли добавить новую точку
|
||||
if (shouldAddPoint(newPosition, currentTime)) {
|
||||
PathPoint newPoint = new PathPoint(newPosition, currentTime, speed, course);
|
||||
pathHistory.offer(newPoint);
|
||||
|
||||
// Ограничиваем количество точек
|
||||
while (pathHistory.size() > MAX_PATH_POINTS) {
|
||||
pathHistory.poll();
|
||||
}
|
||||
|
||||
lastPosition = newPosition;
|
||||
lastUpdateTime = currentTime;
|
||||
|
||||
Log.d(TAG, "Added new point to path for vessel: " + vesselId +
|
||||
", total points: " + pathHistory.size());
|
||||
|
||||
// Обновляем отображение пути
|
||||
updatePathDisplay();
|
||||
} else {
|
||||
Log.d(TAG, "Point not added for vessel: " + vesselId +
|
||||
" (time or distance filter)");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли добавить новую точку в путь
|
||||
*/
|
||||
private boolean shouldAddPoint(Point newPosition, long currentTime) {
|
||||
// Проверяем время
|
||||
if (currentTime - lastUpdateTime < MIN_TIME_BETWEEN_POINTS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем расстояние
|
||||
if (lastPosition != null) {
|
||||
double distance = calculateDistance(lastPosition, newPosition);
|
||||
if (distance < MIN_DISTANCE_BETWEEN_POINTS) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет отображение пути на карте
|
||||
*/
|
||||
private void updatePathDisplay() {
|
||||
Log.d(TAG, "updatePathDisplay called for vessel: " + vesselId);
|
||||
|
||||
if (pathHistory.isEmpty()) {
|
||||
Log.d(TAG, "Path history is empty for vessel: " + vesselId);
|
||||
return;
|
||||
}
|
||||
if (mapObjects == null) {
|
||||
Log.d(TAG, "MapObjects is null for vessel: " + vesselId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем список точек для пройденного пути
|
||||
List<Point> pathPoints = new ArrayList<>();
|
||||
for (PathPoint point : pathHistory) {
|
||||
pathPoints.add(point.position);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Creating path line with " + pathPoints.size() + " points for vessel: " + vesselId);
|
||||
|
||||
// Удаляем старые линии
|
||||
try {
|
||||
if (pathLine != null) {
|
||||
Log.d(TAG, "Removing old path line for vessel: " + vesselId);
|
||||
mapObjects.remove(pathLine);
|
||||
pathLine = null;
|
||||
}
|
||||
if (predictionLine != null) {
|
||||
Log.d(TAG, "Removing old prediction line for vessel: " + vesselId);
|
||||
mapObjects.remove(predictionLine);
|
||||
predictionLine = null;
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Error removing old lines for vessel: " + vesselId, e);
|
||||
// Коллекция могла быть инвалидирована (weak_ptr expired). Прекращаем обновления.
|
||||
isEnabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Создаем линию пройденного пути
|
||||
if (pathPoints.size() > 1) {
|
||||
try {
|
||||
Log.d(TAG, "Adding new path line for vessel: " + vesselId);
|
||||
pathLine = mapObjects.addPolyline(new com.yandex.mapkit.geometry.Polyline(pathPoints));
|
||||
if (pathLine != null) {
|
||||
pathLine.setStrokeColor(pathColor);
|
||||
pathLine.setStrokeWidth(pathWidth);
|
||||
Log.d(TAG, "Path line created successfully for vessel: " + vesselId +
|
||||
", color: " + Integer.toHexString(pathColor) +
|
||||
", width: " + pathWidth);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to create path line for vessel: " + vesselId);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Error creating path line for vessel: " + vesselId, e);
|
||||
isEnabled = false;
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Not enough points for path line for vessel: " + vesselId +
|
||||
" (need at least 2, have " + pathPoints.size() + ")");
|
||||
}
|
||||
|
||||
// Создаем линию прогнозируемого движения
|
||||
createPredictionLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает линию прогнозируемого движения
|
||||
*/
|
||||
private void createPredictionLine() {
|
||||
Log.d(TAG, "createPredictionLine called for vessel: " + vesselId);
|
||||
|
||||
if (pathHistory.isEmpty()) {
|
||||
Log.d(TAG, "Path history is empty for prediction line for vessel: " + vesselId);
|
||||
return;
|
||||
}
|
||||
if (mapObjects == null) {
|
||||
Log.d(TAG, "MapObjects is null for prediction line for vessel: " + vesselId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Получаем последнюю точку
|
||||
PathPoint lastPoint = null;
|
||||
for (PathPoint point : pathHistory) {
|
||||
lastPoint = point;
|
||||
}
|
||||
|
||||
if (lastPoint == null || lastPoint.speed <= 0) {
|
||||
Log.d(TAG, "Cannot create prediction line for vessel: " + vesselId +
|
||||
" (lastPoint: " + (lastPoint != null ? "not null" : "null") +
|
||||
", speed: " + (lastPoint != null ? lastPoint.speed : "N/A") + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
// Рассчитываем прогнозируемую позицию через 1 минуту
|
||||
double predictionTimeMinutes = 1.0; // 1 минута
|
||||
double predictionDistance = lastPoint.speed * predictionTimeMinutes * 60.0; // расстояние в метрах
|
||||
|
||||
Log.d(TAG, "Creating prediction line for vessel: " + vesselId +
|
||||
", speed: " + lastPoint.speed +
|
||||
", course: " + lastPoint.course +
|
||||
", prediction distance: " + predictionDistance + "m");
|
||||
|
||||
// Конвертируем курс в радианы
|
||||
double courseRad = Math.toRadians(lastPoint.course);
|
||||
|
||||
// Рассчитываем новую позицию
|
||||
double earthRadius = 6371000; // радиус Земли в метрах
|
||||
double lat1 = Math.toRadians(lastPoint.position.getLatitude());
|
||||
double lon1 = Math.toRadians(lastPoint.position.getLongitude());
|
||||
|
||||
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(predictionDistance / earthRadius) +
|
||||
Math.cos(lat1) * Math.sin(predictionDistance / earthRadius) * Math.cos(courseRad));
|
||||
|
||||
double lon2 = lon1 + Math.atan2(Math.sin(courseRad) * Math.sin(predictionDistance / earthRadius) * Math.cos(lat1),
|
||||
Math.cos(predictionDistance / earthRadius) - Math.sin(lat1) * Math.sin(lat2));
|
||||
|
||||
Point predictionPoint = new Point(Math.toDegrees(lat2), Math.toDegrees(lon2));
|
||||
|
||||
// Создаем линию прогноза
|
||||
List<Point> predictionPoints = new ArrayList<>();
|
||||
predictionPoints.add(lastPoint.position);
|
||||
predictionPoints.add(predictionPoint);
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Adding prediction line for vessel: " + vesselId);
|
||||
predictionLine = mapObjects.addPolyline(new com.yandex.mapkit.geometry.Polyline(predictionPoints));
|
||||
if (predictionLine != null) {
|
||||
predictionLine.setStrokeColor(predictionColor);
|
||||
predictionLine.setStrokeWidth(predictionWidth);
|
||||
Log.d(TAG, "Prediction line created successfully for vessel: " + vesselId +
|
||||
", color: " + Integer.toHexString(predictionColor) +
|
||||
", width: " + predictionWidth);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to create prediction line for vessel: " + vesselId);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.e(TAG, "Error creating prediction line for vessel: " + vesselId, e);
|
||||
isEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает расстояние между двумя точками в метрах
|
||||
*/
|
||||
private double calculateDistance(Point point1, Point point2) {
|
||||
double lat1 = Math.toRadians(point1.getLatitude());
|
||||
double lon1 = Math.toRadians(point1.getLongitude());
|
||||
double lat2 = Math.toRadians(point2.getLatitude());
|
||||
double lon2 = Math.toRadians(point2.getLongitude());
|
||||
|
||||
double dlat = lat2 - lat1;
|
||||
double dlon = lon2 - lon1;
|
||||
|
||||
double a = Math.sin(dlat / 2) * Math.sin(dlat / 2) +
|
||||
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlon / 2) * Math.sin(dlon / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return 6371000 * c; // радиус Земли в метрах
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает путь судна
|
||||
*/
|
||||
public void clearPath() {
|
||||
try {
|
||||
if (pathLine != null && mapObjects != null) {
|
||||
mapObjects.remove(pathLine);
|
||||
pathLine = null;
|
||||
}
|
||||
if (predictionLine != null && mapObjects != null) {
|
||||
mapObjects.remove(predictionLine);
|
||||
predictionLine = null;
|
||||
}
|
||||
} catch (RuntimeException ignored) {
|
||||
// Игнорируем ошибки очистки при невалидной коллекции
|
||||
}
|
||||
|
||||
|
||||
pathHistory.clear();
|
||||
lastPosition = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет трекер пути
|
||||
*/
|
||||
public void remove() {
|
||||
clearPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает отображение пути
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.isEnabled = enabled;
|
||||
if (!enabled) {
|
||||
clearPath();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает цвет пройденного пути
|
||||
*/
|
||||
public void setPathColor(int color) {
|
||||
this.pathColor = color;
|
||||
if (pathLine != null) {
|
||||
pathLine.setStrokeColor(color);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает цвет прогнозируемого пути
|
||||
*/
|
||||
public void setPredictionColor(int color) {
|
||||
this.predictionColor = color;
|
||||
if (predictionLine != null) {
|
||||
predictionLine.setStrokeColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает ширину линий
|
||||
*/
|
||||
public void setLineWidth(float pathWidth, float predictionWidth) {
|
||||
this.pathWidth = pathWidth;
|
||||
this.predictionWidth = predictionWidth;
|
||||
|
||||
if (pathLine != null) {
|
||||
pathLine.setStrokeWidth(pathWidth);
|
||||
}
|
||||
if (predictionLine != null) {
|
||||
predictionLine.setStrokeWidth(predictionWidth);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, активен ли трекер
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return isEnabled && !pathHistory.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает количество точек в пути
|
||||
*/
|
||||
public int getPathPointCount() {
|
||||
return pathHistory.size();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@ import android.view.View;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.view.CursorOverlay;
|
||||
import com.grigowashere.aismap.R;
|
||||
import com.yandex.mapkit.Animation;
|
||||
import android.view.ViewGroup;
|
||||
import com.yandex.mapkit.geometry.Point;
|
||||
import com.yandex.mapkit.map.CameraPosition;
|
||||
import com.yandex.mapkit.map.MapObjectCollection;
|
||||
@@ -22,6 +25,7 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* Реализация карты для Яндекс.Карт
|
||||
* Использует новый менеджер маркеров для предотвращения финализации объектов
|
||||
*/
|
||||
public class YandexMapImpl implements MapInterface {
|
||||
|
||||
@@ -30,49 +34,70 @@ public class YandexMapImpl implements MapInterface {
|
||||
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 YandexMarkerManager markerManager;
|
||||
|
||||
// Флаги для отслеживания состояния обработчиков
|
||||
private boolean ownVesselClickListenerSet = false;
|
||||
private Map<String, Boolean> aisVesselClickListenersSet = new HashMap<>();
|
||||
// Слушатель поворота карты
|
||||
private com.yandex.mapkit.map.InputListener inputListener;
|
||||
private float lastMapAzimuth = 0.0f;
|
||||
|
||||
// Курсор overlay
|
||||
private CursorOverlay cursorOverlay;
|
||||
private Vessel ownVessel;
|
||||
|
||||
public YandexMapImpl(Context context, MapView mapView) {
|
||||
this.context = context;
|
||||
this.mapView = mapView;
|
||||
this.aisMarkers = new HashMap<>();
|
||||
this.aisVessels = new HashMap<>();
|
||||
this.cursorOverlay = new CursorOverlay(context);
|
||||
|
||||
android.util.Log.d("YandexMapImpl", "Конструктор YandexMapImpl вызван");
|
||||
android.util.Log.d("YandexMapImpl", "Context: " + (context != null ? "установлен" : "null"));
|
||||
android.util.Log.d("YandexMapImpl", "MapView: " + (mapView != null ? "установлен" : "null"));
|
||||
// Добавляем overlay курсора в MapView
|
||||
if (mapView instanceof ViewGroup) {
|
||||
ViewGroup parent = (ViewGroup) mapView;
|
||||
// Проверяем, не добавлен ли уже курсор
|
||||
if (parent.findViewById(R.id.cursor_cross) == null) {
|
||||
parent.addView(cursorOverlay.getView());
|
||||
}
|
||||
}
|
||||
|
||||
// Получение коллекции объектов карты
|
||||
try {
|
||||
this.mapObjects = mapView.getMap().getMapObjects().addCollection();
|
||||
android.util.Log.d("YandexMapImpl", "Коллекция объектов карты создана: " + (mapObjects != null ? "успешно" : "null"));
|
||||
// Инициализируем менеджер маркеров
|
||||
com.grigowashere.aismap.utils.SettingsManager settingsManager =
|
||||
new com.grigowashere.aismap.utils.SettingsManager(context);
|
||||
this.markerManager = new YandexMarkerManager(context, mapObjects, mapView, settingsManager);
|
||||
} 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"));
|
||||
// Инициализируем слушатель поворота карты
|
||||
setupCameraListener();
|
||||
|
||||
// Карта уже инициализирована в конструкторе
|
||||
if (mapObjects != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Коллекция объектов карты готова к использованию");
|
||||
// Инициализируем слушатель движения карты
|
||||
setupMapMovementListener();
|
||||
|
||||
// Инициализируем менеджер маркеров
|
||||
if (markerManager != null) {
|
||||
markerManager.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Очищаем менеджер маркеров
|
||||
if (markerManager != null) {
|
||||
markerManager.cleanup();
|
||||
}
|
||||
|
||||
// Удаляем слушатель ввода
|
||||
if (inputListener != null && mapView != null) {
|
||||
mapView.getMap().removeInputListener(inputListener);
|
||||
}
|
||||
|
||||
if (mapObjects != null) {
|
||||
mapView.getMap().getMapObjects().remove(mapObjects);
|
||||
}
|
||||
@@ -83,216 +108,58 @@ public class YandexMapImpl implements MapInterface {
|
||||
|
||||
@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 (cursorOverlay != null) {
|
||||
cursorOverlay.setOwnVessel(vessel);
|
||||
}
|
||||
|
||||
if (ownVesselMarker != null) {
|
||||
android.util.Log.d("YandexMapImpl", "Удаляем существующий маркер");
|
||||
mapObjects.remove(ownVesselMarker);
|
||||
if (markerManager != null) {
|
||||
markerManager.updateOwnVesselMarker(vessel);
|
||||
}
|
||||
|
||||
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 (cursorOverlay != null) {
|
||||
cursorOverlay.setOwnVessel(vessel);
|
||||
}
|
||||
|
||||
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());
|
||||
if (markerManager != null) {
|
||||
markerManager.updateOwnVesselMarker(vessel);
|
||||
}
|
||||
|
||||
// Обновляем позицию маркера
|
||||
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"));
|
||||
if (markerManager != null) {
|
||||
markerManager.updateAISVesselMarker(vessel);
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
||||
if (markerManager != null) {
|
||||
markerManager.updateAISVesselMarker(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAISVesselMarker(String mmsi) {
|
||||
com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.remove(mmsi);
|
||||
if (marker != null) {
|
||||
mapObjects.remove(marker);
|
||||
if (markerManager != null) {
|
||||
markerManager.removeAISVesselMarker(mmsi);
|
||||
}
|
||||
// Удаляем ссылку на судно
|
||||
aisVessels.remove(mmsi);
|
||||
// Удаляем флаг обработчика кликов
|
||||
aisVesselClickListenersSet.remove(mmsi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAISVesselMarkers() {
|
||||
for (com.yandex.mapkit.map.PlacemarkMapObject marker : aisMarkers.values()) {
|
||||
mapObjects.remove(marker);
|
||||
if (markerManager != null) {
|
||||
markerManager.clearAISVesselMarkers();
|
||||
}
|
||||
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);
|
||||
CameraPosition cameraPosition = new CameraPosition(point, 13.0f, 0.0f, 0.0f);
|
||||
mapView.getMap().move(cameraPosition, new Animation(Animation.Type.SMOOTH, 1.0f), null);
|
||||
}
|
||||
|
||||
@@ -322,102 +189,134 @@ public class YandexMapImpl implements MapInterface {
|
||||
|
||||
@Override
|
||||
public void setMarkerClickListener(MarkerClickListener listener) {
|
||||
android.util.Log.d("YandexMapImpl", "setMarkerClickListener вызван: " + (listener != null ? "listener установлен" : "listener == null"));
|
||||
this.markerClickListener = listener;
|
||||
|
||||
// Переустанавливаем обработчики кликов для всех существующих маркеров
|
||||
updateAllMarkerClickListeners();
|
||||
// Устанавливаем обработчик в менеджере маркеров
|
||||
if (markerManager != null) {
|
||||
markerManager.setMarkerClickListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет обработчики кликов для всех существующих маркеров
|
||||
* Этот метод переустанавливает обработчики для всех маркеров
|
||||
*/
|
||||
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);
|
||||
}
|
||||
public void refreshMarkerClickListeners() {
|
||||
if (markerManager != null) {
|
||||
markerManager.checkAndRestoreMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создание иконки судна
|
||||
* Перерисовывает все маркеры с учетом текущего азимута карты
|
||||
* Вызывается при повороте карты
|
||||
*/
|
||||
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;
|
||||
public void refreshAllMarkers() {
|
||||
if (markerManager != null) {
|
||||
markerManager.refreshAllMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет все маркеры при повороте карты
|
||||
* Вызывается из слушателя поворота карты
|
||||
*/
|
||||
public void onMapRotationChanged() {
|
||||
if (markerManager != null) {
|
||||
markerManager.refreshAllMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно обновляет все маркеры
|
||||
* Можно вызывать извне для обновления маркеров
|
||||
*/
|
||||
public void forceRefreshMarkers() {
|
||||
if (markerManager != null) {
|
||||
markerManager.refreshAllMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно обновляет все маркеры при изменении зума
|
||||
*/
|
||||
public void forceRefreshMarkersOnZoomChange() {
|
||||
if (markerManager != null) {
|
||||
markerManager.forceRefreshAllMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет и восстанавливает финализированные маркеры
|
||||
*/
|
||||
public void checkAndRestoreMarkers() {
|
||||
if (markerManager != null) {
|
||||
markerManager.checkAndRestoreMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает количество активных маркеров
|
||||
*/
|
||||
public int getActiveMarkerCount() {
|
||||
if (markerManager != null) {
|
||||
return markerManager.getActiveMarkerCount();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает отображение путей движения
|
||||
*/
|
||||
public void setPathTrackingEnabled(boolean enabled) {
|
||||
if (markerManager != null) {
|
||||
markerManager.setPathTrackingEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает путь конкретного судна
|
||||
*/
|
||||
public void clearVesselPath(String mmsi) {
|
||||
if (markerManager != null) {
|
||||
markerManager.clearVesselPath(mmsi);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает трекер пути собственного судна
|
||||
*/
|
||||
@Override
|
||||
public void clearVesselPath() {
|
||||
if (markerManager != null) {
|
||||
markerManager.clearVesselPath("own_vessel");
|
||||
}
|
||||
|
||||
// Также очищаем VesselPathController если он используется
|
||||
// (для MapLibre это делается в MapLibreMapImpl, для Yandex - здесь)
|
||||
// В YandexMapImpl VesselPathController не используется напрямую,
|
||||
// но если в будущем будет использоваться, нужно добавить очистку
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает все пути движения
|
||||
*/
|
||||
public void clearAllPaths() {
|
||||
if (markerManager != null) {
|
||||
markerManager.clearAllPaths();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет настройки отображения путей
|
||||
*/
|
||||
public void updatePathSettings(int pathColor, int predictionColor, float pathWidth, float predictionWidth) {
|
||||
if (markerManager != null) {
|
||||
markerManager.updatePathSettings(pathColor, predictionColor, pathWidth, predictionWidth);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Получение MapView для использования в layout
|
||||
*/
|
||||
@@ -426,108 +325,140 @@ public class YandexMapImpl implements MapInterface {
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно пересоздает маркер нашего судна с иконкой
|
||||
* Настройка слушателя поворота карты
|
||||
*/
|
||||
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) {
|
||||
private void setupCameraListener() {
|
||||
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;
|
||||
inputListener = new com.yandex.mapkit.map.InputListener() {
|
||||
@Override
|
||||
public void onMapTap(com.yandex.mapkit.map.Map map, com.yandex.mapkit.geometry.Point point) {
|
||||
// Не обрабатываем клики по карте
|
||||
}
|
||||
|
||||
// Если программная иконка не создалась, пробуем ресурс
|
||||
int iconResId = context.getResources().getIdentifier(iconName, "drawable", context.getPackageName());
|
||||
android.util.Log.d("YandexMapImpl", "ID ресурса " + iconName + ": " + iconResId);
|
||||
@Override
|
||||
public void onMapLongTap(com.yandex.mapkit.map.Map map, com.yandex.mapkit.geometry.Point point) {
|
||||
// Не обрабатываем долгие клики по карте
|
||||
}
|
||||
};
|
||||
|
||||
if (iconResId != 0) {
|
||||
android.util.Log.d("YandexMapImpl", "Устанавливаем иконку из ресурса...");
|
||||
marker.setIcon(ImageProvider.fromResource(context, iconResId));
|
||||
android.util.Log.d("YandexMapImpl", "Иконка " + iconName + " установлена успешно");
|
||||
// Добавляем слушатель к карте
|
||||
mapView.getMap().addInputListener(inputListener);
|
||||
|
||||
// Включаем жесты поворота карты
|
||||
mapView.getMap().setRotateGesturesEnabled(true);
|
||||
|
||||
// Добавляем слушатель изменений камеры для обновления маркеров при повороте и зуме
|
||||
mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() {
|
||||
private long lastUpdateTime = 0;
|
||||
private static final long UPDATE_THROTTLE = 200; // 200мс между обновлениями (увеличено для снижения нагрузки)
|
||||
private float lastZoom = -1;
|
||||
|
||||
@Override
|
||||
public void onCameraPositionChanged(com.yandex.mapkit.map.Map map,
|
||||
com.yandex.mapkit.map.CameraPosition cameraPosition,
|
||||
com.yandex.mapkit.map.CameraUpdateReason reason,
|
||||
boolean finished) {
|
||||
|
||||
// Обновляем маркеры в реальном времени с throttling
|
||||
long currentTime = System.currentTimeMillis();
|
||||
float currentZoom = cameraPosition.getZoom();
|
||||
|
||||
// Проверяем, изменился ли зум значительно (больше чем на 1.0)
|
||||
boolean zoomChanged = Math.abs(currentZoom - lastZoom) > 1.0f;
|
||||
|
||||
if (currentTime - lastUpdateTime >= UPDATE_THROTTLE || zoomChanged) {
|
||||
//onMapRotationChanged();
|
||||
// Обновляем маркеры только при значительных изменениях
|
||||
if (zoomChanged) {
|
||||
// При изменении зума принудительно обновляем все маркеры
|
||||
forceRefreshMarkersOnZoomChange();
|
||||
} 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 иконка установлена");
|
||||
// При повороте только проверяем валидность маркеров
|
||||
checkAndRestoreMarkers();
|
||||
}
|
||||
lastUpdateTime = currentTime;
|
||||
lastZoom = currentZoom;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем дополнительный слушатель для жестов поворота
|
||||
mapView.getMap().addInputListener(new com.yandex.mapkit.map.InputListener() {
|
||||
private long lastGestureTime = 0;
|
||||
private static final long GESTURE_THROTTLE = 100; // 100мс между обновлениями
|
||||
|
||||
@Override
|
||||
public void onMapTap(com.yandex.mapkit.map.Map map, com.yandex.mapkit.geometry.Point point) {
|
||||
// Не обрабатываем клики по карте
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMapLongTap(com.yandex.mapkit.map.Map map, com.yandex.mapkit.geometry.Point point) {
|
||||
// Не обрабатываем долгие клики по карте
|
||||
}
|
||||
});
|
||||
} 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!");
|
||||
@Override
|
||||
public void showCursor() {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.showCursor();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hideCursor() {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.hideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCursorCoordinates(double latitude, double longitude) {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.updateCursorCoordinates(latitude, longitude);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCursorFromMapCenter() {
|
||||
if (cursorOverlay != null && mapView != null) {
|
||||
// Получаем координаты центра карты
|
||||
com.yandex.mapkit.geometry.Point center = mapView.getMap().getCameraPosition().getTarget();
|
||||
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAisVesselInfo(com.grigowashere.aismap.models.AISVessel vessel) {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.setAisVesselInfo(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAisVesselInfo() {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.clearAisVesselInfo();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает слушатель движения карты для обновления курсора
|
||||
*/
|
||||
private void setupMapMovementListener() {
|
||||
if (mapView != null) {
|
||||
mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() {
|
||||
@Override
|
||||
public void onCameraPositionChanged(com.yandex.mapkit.map.Map map, com.yandex.mapkit.map.CameraPosition cameraPosition, com.yandex.mapkit.map.CameraUpdateReason cameraUpdateReason, boolean finished) {
|
||||
// Обновляем координаты курсора при движении карты
|
||||
updateCursorFromMapCenter();
|
||||
}
|
||||
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,634 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.yandex.mapkit.map.MapObjectCollection;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Менеджер маркеров для Yandex Maps
|
||||
* Управляет жизненным циклом маркеров и предотвращает их финализацию
|
||||
*/
|
||||
public class YandexMarkerManager implements MarkerManager {
|
||||
|
||||
private static final String TAG = "YandexMarkerManager";
|
||||
|
||||
private Context context;
|
||||
private MapObjectCollection mapObjects;
|
||||
private com.yandex.mapkit.mapview.MapView mapView;
|
||||
private MapInterface.MarkerClickListener markerClickListener;
|
||||
private com.grigowashere.aismap.utils.SettingsManager settingsManager;
|
||||
|
||||
// Кеш маркеров с управлением жизненным циклом
|
||||
private Map<String, YandexMarkerWrapper> markerCache = new ConcurrentHashMap<>();
|
||||
private YandexMarkerWrapper ownVesselMarker;
|
||||
|
||||
// Трекеры путей движения судов
|
||||
private Map<String, VesselPathTracker> pathTrackers = new ConcurrentHashMap<>();
|
||||
private VesselPathTracker ownVesselPathTracker;
|
||||
private boolean pathTrackingEnabled = true;
|
||||
|
||||
// Периодическая очистка устаревших маркеров
|
||||
private Handler cleanupHandler;
|
||||
private Runnable cleanupRunnable;
|
||||
private static final long CLEANUP_INTERVAL = 10000; // 10 секунд
|
||||
|
||||
// Периодическое обновление маркеров для предотвращения финализации
|
||||
private Handler refreshHandler;
|
||||
private Runnable refreshRunnable;
|
||||
private static final long REFRESH_INTERVAL = 2000; // 2 секунды
|
||||
|
||||
public YandexMarkerManager(Context context, MapObjectCollection mapObjects, com.yandex.mapkit.mapview.MapView mapView, com.grigowashere.aismap.utils.SettingsManager settingsManager) {
|
||||
this.context = context;
|
||||
this.mapObjects = mapObjects;
|
||||
this.mapView = mapView;
|
||||
this.settingsManager = settingsManager;
|
||||
this.cleanupHandler = new Handler(Looper.getMainLooper());
|
||||
this.refreshHandler = new Handler(Looper.getMainLooper());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
startPeriodicCleanup();
|
||||
startPeriodicRefresh();
|
||||
|
||||
// Инициализируем настройки путей из SettingsManager
|
||||
if (settingsManager != null) {
|
||||
pathTrackingEnabled = settingsManager.isPathTrackingEnabled();
|
||||
updatePathSettings(
|
||||
settingsManager.getPathColor(),
|
||||
settingsManager.getPredictionColor(),
|
||||
settingsManager.getPathWidth(),
|
||||
settingsManager.getPredictionWidth()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
stopPeriodicCleanup();
|
||||
stopPeriodicRefresh();
|
||||
|
||||
// Удаляем все маркеры
|
||||
for (YandexMarkerWrapper marker : markerCache.values()) {
|
||||
marker.remove();
|
||||
}
|
||||
markerCache.clear();
|
||||
|
||||
if (ownVesselMarker != null) {
|
||||
ownVesselMarker.remove();
|
||||
ownVesselMarker = null;
|
||||
}
|
||||
|
||||
// Очищаем трекеры путей
|
||||
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||
tracker.remove();
|
||||
}
|
||||
pathTrackers.clear();
|
||||
|
||||
if (ownVesselPathTracker != null) {
|
||||
ownVesselPathTracker.remove();
|
||||
ownVesselPathTracker = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateOwnVesselMarker(Vessel vessel) {
|
||||
if (vessel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем валидность координат
|
||||
if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) ||
|
||||
Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// ВСЕГДА пересоздаем маркер для предотвращения финализации
|
||||
if (ownVesselMarker != null) {
|
||||
ownVesselMarker.remove();
|
||||
}
|
||||
|
||||
// Создаем новый маркер
|
||||
ownVesselMarker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, "own_vessel", settingsManager);
|
||||
if (markerClickListener != null) {
|
||||
ownVesselMarker.setClickListener(() -> {
|
||||
if (markerClickListener != null) {
|
||||
markerClickListener.onOwnVesselClick(vessel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем трекер пути для собственного судна
|
||||
updateOwnVesselPath(vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateAISVesselMarker(AISVessel vessel) {
|
||||
if (vessel == null || vessel.getMmsi() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем валидность координат
|
||||
if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) ||
|
||||
Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) {
|
||||
return;
|
||||
}
|
||||
|
||||
String mmsi = vessel.getMmsi();
|
||||
YandexMarkerWrapper marker = markerCache.get(mmsi);
|
||||
|
||||
// ВСЕГДА пересоздаем маркер для предотвращения финализации
|
||||
if (marker != null) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
// Создаем новый маркер
|
||||
marker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, mmsi, settingsManager);
|
||||
markerCache.put(mmsi, marker);
|
||||
|
||||
if (markerClickListener != null) {
|
||||
marker.setClickListener(() -> {
|
||||
if (markerClickListener != null) {
|
||||
markerClickListener.onAISVesselClick(vessel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Обновляем трекер пути для AIS судна
|
||||
updateAISVesselPath(vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAISVesselMarker(String mmsi) {
|
||||
YandexMarkerWrapper marker = markerCache.remove(mmsi);
|
||||
if (marker != null) {
|
||||
marker.remove();
|
||||
}
|
||||
|
||||
// Удаляем трекер пути
|
||||
VesselPathTracker pathTracker = pathTrackers.remove(mmsi);
|
||||
if (pathTracker != null) {
|
||||
pathTracker.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAISVesselMarkers() {
|
||||
for (YandexMarkerWrapper marker : markerCache.values()) {
|
||||
marker.remove();
|
||||
}
|
||||
markerCache.clear();
|
||||
|
||||
// Очищаем все трекеры путей AIS судов
|
||||
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||
tracker.remove();
|
||||
}
|
||||
pathTrackers.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkerClickListener(MapInterface.MarkerClickListener listener) {
|
||||
this.markerClickListener = listener;
|
||||
|
||||
// Устанавливаем обработчики для существующих маркеров
|
||||
if (ownVesselMarker != null && ownVesselMarker.isValid()) {
|
||||
ownVesselMarker.setClickListener(() -> {
|
||||
if (markerClickListener != null && ownVesselMarker.getVessel() != null) {
|
||||
markerClickListener.onOwnVesselClick(ownVesselMarker.getVessel());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (YandexMarkerWrapper marker : markerCache.values()) {
|
||||
if (marker.isValid()) {
|
||||
marker.setClickListener(() -> {
|
||||
if (markerClickListener != null && marker.getAISVessel() != null) {
|
||||
markerClickListener.onAISVesselClick(marker.getAISVessel());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void refreshAllMarkers() {
|
||||
// При повороте карты пересоздаем все маркеры
|
||||
// Это гарантирует правильную ориентацию относительно севера
|
||||
if (mapObjects == null || mapView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Пересоздаем маркер нашего судна
|
||||
if (ownVesselMarker != null) {
|
||||
Vessel vessel = ownVesselMarker.getVessel();
|
||||
if (vessel != null) {
|
||||
ownVesselMarker.remove();
|
||||
updateOwnVesselMarker(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
// Пересоздаем все AIS маркеры
|
||||
Map<String, AISVessel> vesselsToRecreate = new HashMap<>();
|
||||
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
||||
YandexMarkerWrapper marker = entry.getValue();
|
||||
AISVessel vessel = marker.getAISVessel();
|
||||
if (vessel != null) {
|
||||
try {
|
||||
marker.remove();
|
||||
} catch (RuntimeException ignored) {
|
||||
// Игнорируем, если underlying объект недоступен
|
||||
}
|
||||
vesselsToRecreate.put(entry.getKey(), vessel);
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем кеш и пересоздаем маркеры
|
||||
markerCache.clear();
|
||||
for (Map.Entry<String, AISVessel> entry : vesselsToRecreate.entrySet()) {
|
||||
try {
|
||||
updateAISVesselMarker(entry.getValue());
|
||||
} catch (RuntimeException ignored) {
|
||||
// Пропускаем пересоздание при ошибке
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkAndRestoreMarkers() {
|
||||
// Проверяем маркер нашего судна
|
||||
if (ownVesselMarker != null && !ownVesselMarker.isValid()) {
|
||||
Vessel vessel = ownVesselMarker.getVessel();
|
||||
if (vessel != null) {
|
||||
ownVesselMarker.remove();
|
||||
updateOwnVesselMarker(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем AIS маркеры
|
||||
Set<String> toRemove = new HashSet<>();
|
||||
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
||||
YandexMarkerWrapper marker = entry.getValue();
|
||||
if (!marker.isValid()) {
|
||||
AISVessel vessel = marker.getAISVessel();
|
||||
if (vessel != null) {
|
||||
marker.remove();
|
||||
toRemove.add(entry.getKey());
|
||||
updateAISVesselMarker(vessel);
|
||||
} else {
|
||||
toRemove.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем невалидные маркеры
|
||||
for (String mmsi : toRemove) {
|
||||
markerCache.remove(mmsi);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getActiveMarkerCount() {
|
||||
int count = 0;
|
||||
if (ownVesselMarker != null && ownVesselMarker.isValid()) {
|
||||
count++;
|
||||
}
|
||||
for (YandexMarkerWrapper marker : markerCache.values()) {
|
||||
if (marker.isValid()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает периодическую очистку устаревших маркеров
|
||||
*/
|
||||
private void startPeriodicCleanup() {
|
||||
cleanupRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
cleanupExpiredMarkers();
|
||||
cleanupHandler.postDelayed(this, CLEANUP_INTERVAL);
|
||||
}
|
||||
};
|
||||
cleanupHandler.post(cleanupRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает периодическую очистку
|
||||
*/
|
||||
private void stopPeriodicCleanup() {
|
||||
if (cleanupRunnable != null) {
|
||||
cleanupHandler.removeCallbacks(cleanupRunnable);
|
||||
cleanupRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запускает периодическое обновление маркеров
|
||||
*/
|
||||
private void startPeriodicRefresh() {
|
||||
refreshRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
refreshAllMarkers();
|
||||
try {
|
||||
// Проверяем только валидность маркеров, не пересоздаем их
|
||||
checkAndRestoreMarkers();
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e(TAG, "Ошибка при периодическом обновлении маркеров: " + e.getMessage(), e);
|
||||
}
|
||||
// Планируем следующее обновление
|
||||
refreshHandler.postDelayed(this, REFRESH_INTERVAL);
|
||||
}
|
||||
};
|
||||
refreshHandler.post(refreshRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает периодическое обновление
|
||||
*/
|
||||
private void stopPeriodicRefresh() {
|
||||
if (refreshRunnable != null) {
|
||||
refreshHandler.removeCallbacks(refreshRunnable);
|
||||
refreshRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает устаревшие маркеры
|
||||
*/
|
||||
private void cleanupExpiredMarkers() {
|
||||
// Очищаем AIS маркеры
|
||||
Set<String> toRemove = new HashSet<>();
|
||||
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
||||
YandexMarkerWrapper marker = entry.getValue();
|
||||
if (marker.isExpired() || !marker.isValid() || marker.shouldBeRemoved()) {
|
||||
marker.remove();
|
||||
toRemove.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
// Удаляем маркеры и их трекеры путей
|
||||
for (String mmsi : toRemove) {
|
||||
markerCache.remove(mmsi);
|
||||
|
||||
// Удаляем трекер пути для этого судна
|
||||
VesselPathTracker pathTracker = pathTrackers.remove(mmsi);
|
||||
if (pathTracker != null) {
|
||||
pathTracker.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем маркер нашего судна
|
||||
if (ownVesselMarker != null && (ownVesselMarker.isExpired() || !ownVesselMarker.isValid())) {
|
||||
ownVesselMarker.remove();
|
||||
ownVesselMarker = null;
|
||||
|
||||
// Удаляем трекер пути нашего судна
|
||||
if (ownVesselPathTracker != null) {
|
||||
ownVesselPathTracker.remove();
|
||||
ownVesselPathTracker = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно обновляет все маркеры (например, при изменении зума)
|
||||
*/
|
||||
public void forceRefreshAllMarkers() {
|
||||
// Пересоздаем маркер нашего судна
|
||||
if (ownVesselMarker != null) {
|
||||
Vessel vessel = ownVesselMarker.getVessel();
|
||||
if (vessel != null) {
|
||||
ownVesselMarker.remove();
|
||||
updateOwnVesselMarker(vessel);
|
||||
}
|
||||
}
|
||||
|
||||
// Пересоздаем все AIS маркеры
|
||||
Map<String, AISVessel> vesselsToRecreate = new HashMap<>();
|
||||
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
||||
YandexMarkerWrapper marker = entry.getValue();
|
||||
AISVessel vessel = marker.getAISVessel();
|
||||
if (vessel != null) {
|
||||
marker.remove();
|
||||
vesselsToRecreate.put(entry.getKey(), vessel);
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем кеш и трекеры путей
|
||||
markerCache.clear();
|
||||
|
||||
// Очищаем все трекеры путей AIS судов
|
||||
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||
tracker.remove();
|
||||
}
|
||||
pathTrackers.clear();
|
||||
|
||||
// Пересоздаем маркеры
|
||||
for (Map.Entry<String, AISVessel> entry : vesselsToRecreate.entrySet()) {
|
||||
updateAISVesselMarker(entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPathTrackingEnabled(boolean enabled) {
|
||||
this.pathTrackingEnabled = enabled;
|
||||
|
||||
// Сохраняем настройку в SettingsManager
|
||||
if (settingsManager != null) {
|
||||
settingsManager.setPathTrackingEnabled(enabled);
|
||||
}
|
||||
|
||||
// Обновляем состояние всех трекеров
|
||||
if (ownVesselPathTracker != null) {
|
||||
ownVesselPathTracker.setEnabled(enabled);
|
||||
}
|
||||
|
||||
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||
tracker.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearVesselPath(String mmsi) {
|
||||
VesselPathTracker tracker = pathTrackers.get(mmsi);
|
||||
if (tracker != null) {
|
||||
tracker.clearPath();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAllPaths() {
|
||||
if (ownVesselPathTracker != null) {
|
||||
ownVesselPathTracker.clearPath();
|
||||
}
|
||||
|
||||
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||
tracker.clearPath();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePathSettings(int pathColor, int predictionColor, float pathWidth, float predictionWidth) {
|
||||
// Сохраняем настройки в SettingsManager
|
||||
if (settingsManager != null) {
|
||||
settingsManager.setPathColor(pathColor);
|
||||
settingsManager.setPredictionColor(predictionColor);
|
||||
settingsManager.setPathWidth(pathWidth);
|
||||
settingsManager.setPredictionWidth(predictionWidth);
|
||||
}
|
||||
|
||||
// Обновляем настройки всех трекеров
|
||||
if (ownVesselPathTracker != null) {
|
||||
ownVesselPathTracker.setPathColor(pathColor);
|
||||
ownVesselPathTracker.setPredictionColor(predictionColor);
|
||||
ownVesselPathTracker.setLineWidth(pathWidth, predictionWidth);
|
||||
}
|
||||
|
||||
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||
tracker.setPathColor(pathColor);
|
||||
tracker.setPredictionColor(predictionColor);
|
||||
tracker.setLineWidth(pathWidth, predictionWidth);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет трекер пути для собственного судна
|
||||
*/
|
||||
private void updateOwnVesselPath(Vessel vessel) {
|
||||
if (!pathTrackingEnabled || vessel == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, движется ли судно
|
||||
if (!isVesselMoving(vessel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ownVesselPathTracker == null) {
|
||||
ownVesselPathTracker = new VesselPathTracker("own_vessel", mapObjects);
|
||||
}
|
||||
|
||||
ownVesselPathTracker.updatePosition(
|
||||
vessel.getLatitude(),
|
||||
vessel.getLongitude(),
|
||||
vessel.getSpeed(),
|
||||
vessel.getCourse()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет трекер пути для AIS судна
|
||||
*/
|
||||
private void updateAISVesselPath(AISVessel vessel) {
|
||||
if (!pathTrackingEnabled || vessel == null || vessel.getMmsi() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем, движется ли судно
|
||||
if (!isAISVesselMoving(vessel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String mmsi = vessel.getMmsi();
|
||||
VesselPathTracker tracker = pathTrackers.get(mmsi);
|
||||
|
||||
if (tracker == null) {
|
||||
tracker = new VesselPathTracker(mmsi, mapObjects);
|
||||
pathTrackers.put(mmsi, tracker);
|
||||
}
|
||||
|
||||
// Курс для прогноза: HDG (0..359) если валиден, иначе COG
|
||||
double displayCourse = getAISDisplayCourse(vessel);
|
||||
|
||||
tracker.updatePosition(
|
||||
vessel.getLatitude(),
|
||||
vessel.getLongitude(),
|
||||
vessel.getSpeed(),
|
||||
displayCourse
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, движется ли собственное судно
|
||||
*/
|
||||
private boolean isVesselMoving(Vessel vessel) {
|
||||
if (vessel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Считаем, что судно движется, если скорость больше 0.5 узла
|
||||
return vessel.getSpeed() > 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, движется ли AIS судно
|
||||
*/
|
||||
private boolean isAISVesselMoving(AISVessel vessel) {
|
||||
if (vessel == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Проверяем навигационный статус
|
||||
String navStatus = vessel.getNavigationalStatus();
|
||||
if (navStatus != null) {
|
||||
String status = navStatus.toLowerCase();
|
||||
// Считаем, что судно движется, если не стоит на якоре и не пришвартовано
|
||||
if (status.contains("at anchor") ||
|
||||
status.contains("moored") ||
|
||||
status.contains("not under command")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Считаем, что судно движется, если скорость больше 0.5 узла
|
||||
return vessel.getSpeed() > 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает курс для AIS: валидный HDG (0..359), 511 — невалидно; иначе COG
|
||||
*/
|
||||
private double getAISDisplayCourse(AISVessel vessel) {
|
||||
try {
|
||||
double hdg = vessel.getHeading();
|
||||
if (isValidHeading(hdg)) {
|
||||
return normalizeCourse(hdg);
|
||||
}
|
||||
return normalizeCourse(vessel.getCourse());
|
||||
} catch (Exception ignored) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет валидность HDG
|
||||
*/
|
||||
private boolean isValidHeading(double heading) {
|
||||
if (Double.isNaN(heading) || Double.isInfinite(heading)) return false;
|
||||
int h = (int) Math.round(heading);
|
||||
if (h == 511) return false;
|
||||
return h >= 0 && h <= 359;
|
||||
}
|
||||
|
||||
/**
|
||||
* Нормализует курс в диапазон [0, 360)
|
||||
*/
|
||||
private double normalizeCourse(double course) {
|
||||
if (Double.isNaN(course) || Double.isInfinite(course)) return 0.0;
|
||||
double c = course % 360.0;
|
||||
if (c < 0) c += 360.0;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,721 @@
|
||||
package com.grigowashere.aismap.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.yandex.mapkit.geometry.Point;
|
||||
import com.yandex.mapkit.map.PlacemarkMapObject;
|
||||
import com.yandex.mapkit.map.MapObjectCollection;
|
||||
import com.yandex.runtime.image.ImageProvider;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Обертка для маркера Yandex Maps с управлением жизненным циклом
|
||||
*/
|
||||
public class YandexMarkerWrapper extends MarkerWrapper {
|
||||
|
||||
private Context context;
|
||||
private PlacemarkMapObject marker;
|
||||
private MapObjectCollection mapObjects;
|
||||
private Vessel vessel;
|
||||
private AISVessel aisVessel;
|
||||
private boolean isOwnVessel;
|
||||
private AtomicBoolean clickListenerSet = new AtomicBoolean(false);
|
||||
private Runnable clickHandler;
|
||||
|
||||
// Ссылка на MapView для получения азимута карты
|
||||
private com.yandex.mapkit.mapview.MapView mapView;
|
||||
|
||||
// Кешированные данные для предотвращения лишних обновлений
|
||||
private double lastLatitude = Double.NaN;
|
||||
private double lastLongitude = Double.NaN;
|
||||
private double lastCourse = Double.NaN;
|
||||
private int lastColor = -1;
|
||||
private boolean lastSelected = false;
|
||||
|
||||
// Кеш иконок для быстрого отображения
|
||||
private Bitmap cachedIconBitmap;
|
||||
private double cachedIconCourse = Double.NaN;
|
||||
private int cachedIconColor = -1;
|
||||
private boolean cachedIconSelected = false;
|
||||
private float cachedIconZoom = -1;
|
||||
private boolean cachedIconStale = false;
|
||||
|
||||
// Ссылка на SettingsManager для получения настроек устаревания
|
||||
private com.grigowashere.aismap.utils.SettingsManager settingsManager;
|
||||
|
||||
// Константы для масштабирования маркеров
|
||||
private static final float MIN_MARKER_SIZE = 24f; // Минимальный размер маркера в пикселях (увеличен)
|
||||
private static final float MAX_MARKER_SIZE = 200f; // Максимальный размер маркера в пикселях (увеличен)
|
||||
private static final float ZOOM_THRESHOLD_FOR_REAL_SIZE = 12f; // Зум, при котором начинаем использовать реальные размеры (снижен)
|
||||
private static final float MEDIUM_ZOOM_SIZE = 48f; // Размер маркера на среднем приближении
|
||||
private static final float CLOSE_ZOOM_SIZE = 80f; // Размер маркера на близком приближении
|
||||
|
||||
public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects,
|
||||
com.yandex.mapkit.mapview.MapView mapView, Vessel vessel, String id,
|
||||
com.grigowashere.aismap.utils.SettingsManager settingsManager) {
|
||||
super(id);
|
||||
this.context = context;
|
||||
this.mapObjects = mapObjects;
|
||||
this.mapView = mapView;
|
||||
this.vessel = vessel;
|
||||
this.isOwnVessel = true;
|
||||
this.settingsManager = settingsManager;
|
||||
// Предварительно создаем иконку
|
||||
preloadIcon();
|
||||
createMarker();
|
||||
}
|
||||
|
||||
public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects,
|
||||
com.yandex.mapkit.mapview.MapView mapView, AISVessel vessel, String id,
|
||||
com.grigowashere.aismap.utils.SettingsManager settingsManager) {
|
||||
super(id);
|
||||
this.context = context;
|
||||
this.mapObjects = mapObjects;
|
||||
this.mapView = mapView;
|
||||
this.aisVessel = vessel;
|
||||
this.isOwnVessel = false;
|
||||
this.settingsManager = settingsManager;
|
||||
// Предварительно создаем иконку
|
||||
preloadIcon();
|
||||
createMarker();
|
||||
}
|
||||
|
||||
/**
|
||||
* Предварительно создает иконку для быстрого отображения
|
||||
*/
|
||||
private void preloadIcon() {
|
||||
try {
|
||||
// Курс для поворота: HDG (0..359) если валиден, иначе COG
|
||||
double course = getDisplayCourse();
|
||||
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||
boolean stale = isDataStale(); // Проверяем устаревание данных
|
||||
|
||||
cachedIconBitmap = createRotatedIcon(course, color, selected, stale);
|
||||
cachedIconCourse = course;
|
||||
cachedIconColor = color;
|
||||
cachedIconSelected = selected;
|
||||
cachedIconStale = stale;
|
||||
} catch (Exception e) {
|
||||
// Ошибка предварительной загрузки иконки
|
||||
cachedIconBitmap = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void createMarker() {
|
||||
try {
|
||||
double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLatitude();
|
||||
double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude();
|
||||
|
||||
// Сначала создаем иконку
|
||||
Bitmap iconBitmap = createIconBitmap();
|
||||
|
||||
Point point = new Point(lat, lon);
|
||||
marker = mapObjects.addPlacemark(point);
|
||||
|
||||
if (marker != null) {
|
||||
// Сразу устанавливаем готовую иконку
|
||||
if (iconBitmap != null) {
|
||||
marker.setIcon(ImageProvider.fromBitmap(iconBitmap));
|
||||
} else {
|
||||
// Fallback иконка
|
||||
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
|
||||
}
|
||||
setupClickListener();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ошибка создания маркера
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает иконку маркера заранее
|
||||
*/
|
||||
private Bitmap createIconBitmap() {
|
||||
try {
|
||||
// Курс для поворота: HDG (0..359) если валиден, иначе COG
|
||||
double course = getDisplayCourse();
|
||||
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||
boolean stale = isDataStale(); // Проверяем устаревание данных
|
||||
|
||||
return createRotatedIcon(course, color, selected, stale);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Пересоздает маркер с новыми координатами
|
||||
* Этот метод больше не используется - маркеры всегда пересоздаются в менеджере
|
||||
*/
|
||||
private void recreateMarker(double latitude, double longitude) {
|
||||
// Метод оставлен для совместимости, но не используется
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает иконку немедленно без проверок
|
||||
*/
|
||||
private void setIconImmediately() {
|
||||
try {
|
||||
// Курс для поворота: HDG (0..359) если валиден, иначе COG
|
||||
double course = getDisplayCourse();
|
||||
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||
boolean stale = isDataStale(); // Проверяем устаревание данных
|
||||
|
||||
// Получаем текущий зум для проверки кеша
|
||||
float currentZoom = getCurrentZoom();
|
||||
|
||||
// Проверяем кеш иконки
|
||||
Bitmap iconBitmap = null;
|
||||
if (Double.compare(course, cachedIconCourse) == 0 &&
|
||||
color == cachedIconColor &&
|
||||
selected == cachedIconSelected &&
|
||||
stale == cachedIconStale &&
|
||||
Float.compare(currentZoom, cachedIconZoom) == 0 &&
|
||||
cachedIconBitmap != null) {
|
||||
// Используем кешированную иконку
|
||||
iconBitmap = cachedIconBitmap;
|
||||
} else {
|
||||
// Создаем новую иконку
|
||||
iconBitmap = createRotatedIcon(course, color, selected, stale);
|
||||
if (iconBitmap != null) {
|
||||
// Кешируем иконку
|
||||
cachedIconBitmap = iconBitmap;
|
||||
cachedIconCourse = course;
|
||||
cachedIconColor = color;
|
||||
cachedIconSelected = selected;
|
||||
cachedIconStale = stale;
|
||||
cachedIconZoom = currentZoom;
|
||||
}
|
||||
}
|
||||
|
||||
if (iconBitmap != null) {
|
||||
marker.setIcon(ImageProvider.fromBitmap(iconBitmap));
|
||||
} else {
|
||||
// Fallback иконка если не удалось создать повернутую
|
||||
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
|
||||
}
|
||||
|
||||
// Обновляем кешированные значения
|
||||
lastCourse = course;
|
||||
lastColor = color;
|
||||
lastSelected = selected;
|
||||
} catch (Exception e) {
|
||||
// Ошибка установки иконки - используем fallback
|
||||
try {
|
||||
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
|
||||
} catch (Exception ex) {
|
||||
// Игнорируем ошибки fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
try {
|
||||
if (marker == null) {
|
||||
return false;
|
||||
}
|
||||
// Пробуем получить геометрию для проверки состояния
|
||||
marker.getGeometry();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updatePosition(double latitude, double longitude) {
|
||||
// Этот метод больше не используется - маркеры всегда пересоздаются
|
||||
// Оставляем для совместимости с интерфейсом
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateCourse(double course) {
|
||||
// Этот метод больше не используется - маркеры всегда пересоздаются
|
||||
// Оставляем для совместимости с интерфейсом
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
try {
|
||||
if (marker != null) {
|
||||
mapObjects.remove(marker);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Игнорируем ошибки при удалении
|
||||
} finally {
|
||||
deactivate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIcon() {
|
||||
// Этот метод больше не используется - маркеры всегда пересоздаются
|
||||
// Оставляем для совместимости с интерфейсом
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClickListener(Runnable clickHandler) {
|
||||
this.clickHandler = clickHandler;
|
||||
setupClickListener();
|
||||
}
|
||||
|
||||
private void setupClickListener() {
|
||||
if (marker == null || clickHandler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Сбрасываем флаг для возможности повторной установки
|
||||
clickListenerSet.set(false);
|
||||
|
||||
try {
|
||||
marker.addTapListener((mapObject, point) -> {
|
||||
try {
|
||||
if (mapObject != null && clickHandler != null) {
|
||||
clickHandler.run();
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
clickListenerSet.set(true);
|
||||
} catch (Exception e) {
|
||||
// Ошибка установки обработчика кликов
|
||||
clickListenerSet.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap createRotatedIcon(double course, int color, boolean isSelected, boolean isStale) {
|
||||
// Получаем текущий зум карты
|
||||
float currentZoom = getCurrentZoom();
|
||||
|
||||
try {
|
||||
// Сначала выбираем базовую иконку: для AIS Class A используем targetclassa
|
||||
String baseIconName = (!isOwnVessel && isAISClassA()) ? "targetclassa" : "target";
|
||||
int targetIconResId = context.getResources().getIdentifier(baseIconName, "drawable", context.getPackageName());
|
||||
if (targetIconResId == 0) {
|
||||
return createSimpleIcon(color, course, currentZoom, isStale);
|
||||
}
|
||||
|
||||
Drawable targetDrawable = context.getResources().getDrawable(targetIconResId, null);
|
||||
if (targetDrawable == null) {
|
||||
return createSimpleIcon(color, course, currentZoom, isStale);
|
||||
}
|
||||
|
||||
// Получаем иконку losingtarget для наложения (если данные устарели)
|
||||
Drawable losingTargetDrawable = null;
|
||||
if (isStale) {
|
||||
int losingTargetIconResId = context.getResources().getIdentifier("losingtarget", "drawable", context.getPackageName());
|
||||
if (losingTargetIconResId != 0) {
|
||||
losingTargetDrawable = context.getResources().getDrawable(losingTargetIconResId, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Применяем цвет к основной иконке
|
||||
if (color != 0) {
|
||||
targetDrawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
||||
// Получаем размеры основной иконки
|
||||
int originalWidth = targetDrawable.getIntrinsicWidth();
|
||||
int originalHeight = targetDrawable.getIntrinsicHeight();
|
||||
|
||||
if (originalWidth <= 0) originalWidth = 32;
|
||||
if (originalHeight <= 0) originalHeight = 48;
|
||||
|
||||
// Рассчитываем размер маркера на основе зума и размеров судна
|
||||
float markerSize = calculateMarkerSize(currentZoom);
|
||||
|
||||
// Масштабируем пропорционально рассчитанному размеру
|
||||
float scale = markerSize / Math.max(originalWidth, originalHeight);
|
||||
int width = (int) (originalWidth * scale);
|
||||
int height = (int) (originalHeight * scale);
|
||||
|
||||
// Создаем bitmap с дополнительным пространством для обводки и тени
|
||||
int padding = 12;
|
||||
int bitmapSize = Math.max(width, height) + padding * 2;
|
||||
Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
// Получаем азимут карты (поворот карты)
|
||||
float mapAzimuth = 0.0f;
|
||||
try {
|
||||
if (mapView != null) {
|
||||
com.yandex.mapkit.map.CameraPosition cameraPosition = mapView.getMap().getCameraPosition();
|
||||
mapAzimuth = cameraPosition.getAzimuth();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Не удалось получить азимут карты, используем 0
|
||||
}
|
||||
|
||||
// Поворачиваем основную иконку на курс судна с учетом поворота карты
|
||||
// Курс судна - это направление относительно севера
|
||||
// Азимут карты - это поворот карты относительно севера
|
||||
// Итоговый поворот = курс судна - азимут карты (чтобы маркер оставался относительно севера)
|
||||
float rotationAngle = (float) (course - mapAzimuth);
|
||||
|
||||
int centerX = bitmapSize / 2;
|
||||
int centerY = bitmapSize / 2;
|
||||
int left = centerX - width / 2;
|
||||
int top = centerY - height / 2;
|
||||
|
||||
// Рисуем тень (смещенную копию)
|
||||
targetDrawable.setBounds(left + 2, top + 2, left + width + 2, top + height + 2);
|
||||
targetDrawable.setColorFilter(0x80000000, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||
|
||||
canvas.save();
|
||||
canvas.rotate(rotationAngle, centerX, centerY);
|
||||
targetDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
|
||||
// Рисуем основную иконку target (поворачивается)
|
||||
targetDrawable.setBounds(left, top, left + width, top + height);
|
||||
targetDrawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||
|
||||
canvas.save();
|
||||
canvas.rotate(rotationAngle, centerX, centerY);
|
||||
targetDrawable.draw(canvas);
|
||||
canvas.restore();
|
||||
|
||||
// Рисуем losingtarget поверх (НЕ поворачивается)
|
||||
if (losingTargetDrawable != null) {
|
||||
// Используем тот же размер для losingtarget
|
||||
losingTargetDrawable.setBounds(left, top, left + width, top + height);
|
||||
losingTargetDrawable.draw(canvas); // Без поворота!
|
||||
}
|
||||
|
||||
// Добавляем рамку выделения если нужно
|
||||
if (isSelected) {
|
||||
addSelectionFrame(canvas, centerX, centerY, Math.max(width, height));
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
return createSimpleIcon(color, course, currentZoom, isStale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Возвращает курс для отображения маркера: валидный HDG (0..359), иначе COG
|
||||
*/
|
||||
private double getDisplayCourse() {
|
||||
try {
|
||||
if (isOwnVessel) {
|
||||
double cog = vessel != null ? vessel.getCourse() : 0.0;
|
||||
return normalizeCourse(cog);
|
||||
}
|
||||
if (aisVessel != null) {
|
||||
double hdg = aisVessel.getHeading();
|
||||
if (isValidHeading(hdg)) {
|
||||
return normalizeCourse(hdg);
|
||||
}
|
||||
double cog = aisVessel.getCourse();
|
||||
return normalizeCourse(cog);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка валидности HDG: 0..359 включительно, 511 — невалидно
|
||||
*/
|
||||
private boolean isValidHeading(double heading) {
|
||||
if (Double.isNaN(heading) || Double.isInfinite(heading)) return false;
|
||||
int h = (int) Math.round(heading);
|
||||
if (h == 511) return false;
|
||||
return h >= 0 && h <= 359;
|
||||
}
|
||||
|
||||
/**
|
||||
* Нормализует курс к диапазону [0, 360)
|
||||
*/
|
||||
private double normalizeCourse(double course) {
|
||||
if (Double.isNaN(course) || Double.isInfinite(course)) return 0.0;
|
||||
double c = course % 360.0;
|
||||
if (c < 0) c += 360.0;
|
||||
return c;
|
||||
}
|
||||
|
||||
private boolean isAISClassA() {
|
||||
try {
|
||||
if (aisVessel == null) return false;
|
||||
String cls = aisVessel.getVesselClass();
|
||||
if (cls == null) return false;
|
||||
String s = cls.trim().toLowerCase();
|
||||
return s.equals("class a") || s.equals("a") || s.contains("class a");
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap createSimpleIcon(int color, double course, float zoom, boolean isStale) {
|
||||
try {
|
||||
// Рассчитываем размер маркера на основе зума
|
||||
float markerSize = calculateMarkerSize(zoom);
|
||||
int size = (int) markerSize;
|
||||
|
||||
// Увеличиваем размер bitmap для обводки и тени
|
||||
int padding = 8;
|
||||
int bitmapSize = size + padding * 2;
|
||||
Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
// Смещаем координаты с учетом padding
|
||||
float centerX = bitmapSize / 2f;
|
||||
float centerY = bitmapSize / 2f;
|
||||
|
||||
// Создаем путь для треугольника
|
||||
android.graphics.Path path = new android.graphics.Path();
|
||||
path.moveTo(centerX, padding);
|
||||
path.lineTo(padding + size * 0.1f, padding + size * 0.8f);
|
||||
path.lineTo(padding + size * 0.9f, padding + size * 0.8f);
|
||||
path.close();
|
||||
|
||||
// Рисуем тень (смещенную копию)
|
||||
Paint shadowPaint = new Paint();
|
||||
shadowPaint.setColor(0x80000000); // Полупрозрачный черный
|
||||
shadowPaint.setStyle(Paint.Style.FILL);
|
||||
shadowPaint.setAntiAlias(true);
|
||||
|
||||
canvas.save();
|
||||
canvas.translate(2, 2); // Смещение для тени
|
||||
canvas.rotate((float) course, centerX, centerY);
|
||||
canvas.drawPath(path, shadowPaint);
|
||||
canvas.restore();
|
||||
|
||||
// Рисуем внешнюю обводку
|
||||
Paint outlinePaint = new Paint();
|
||||
outlinePaint.setColor(0xFF000000); // Черная обводка
|
||||
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outlinePaint.setStrokeWidth(4f);
|
||||
outlinePaint.setAntiAlias(true);
|
||||
|
||||
canvas.save();
|
||||
canvas.rotate((float) course, centerX, centerY);
|
||||
canvas.drawPath(path, outlinePaint);
|
||||
canvas.restore();
|
||||
|
||||
// Рисуем внутреннюю обводку
|
||||
Paint innerOutlinePaint = new Paint();
|
||||
innerOutlinePaint.setColor(0xFFFFFFFF); // Белая внутренняя обводка
|
||||
innerOutlinePaint.setStyle(Paint.Style.STROKE);
|
||||
innerOutlinePaint.setStrokeWidth(2f);
|
||||
innerOutlinePaint.setAntiAlias(true);
|
||||
|
||||
canvas.save();
|
||||
canvas.rotate((float) course, centerX, centerY);
|
||||
canvas.drawPath(path, innerOutlinePaint);
|
||||
canvas.restore();
|
||||
|
||||
// Рисуем основную заливку
|
||||
Paint fillPaint = new Paint();
|
||||
fillPaint.setColor(color);
|
||||
fillPaint.setStyle(Paint.Style.FILL);
|
||||
fillPaint.setAntiAlias(true);
|
||||
|
||||
// Для устаревших данных рисуем пунктирный треугольник
|
||||
if (isStale) {
|
||||
fillPaint.setStyle(Paint.Style.STROKE);
|
||||
fillPaint.setStrokeWidth(3f);
|
||||
fillPaint.setPathEffect(new android.graphics.DashPathEffect(new float[]{10, 5}, 0));
|
||||
}
|
||||
|
||||
canvas.save();
|
||||
canvas.rotate((float) course, centerX, centerY);
|
||||
canvas.drawPath(path, fillPaint);
|
||||
canvas.restore();
|
||||
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void addSelectionFrame(Canvas canvas, int centerX, int centerY, int size) {
|
||||
try {
|
||||
// Сначала рисуем тень для рамки выделения
|
||||
Paint shadowPaint = new Paint();
|
||||
shadowPaint.setColor(0x80000000);
|
||||
shadowPaint.setStyle(Paint.Style.STROKE);
|
||||
shadowPaint.setStrokeWidth(6f);
|
||||
shadowPaint.setAntiAlias(true);
|
||||
|
||||
int shadowSize = size + 20;
|
||||
canvas.drawCircle(centerX + 2, centerY + 2, shadowSize / 2, shadowPaint);
|
||||
|
||||
// Рисуем внешнюю обводку
|
||||
Paint outerOutlinePaint = new Paint();
|
||||
outerOutlinePaint.setColor(0xFF000000);
|
||||
outerOutlinePaint.setStyle(Paint.Style.STROKE);
|
||||
outerOutlinePaint.setStrokeWidth(4f);
|
||||
outerOutlinePaint.setAntiAlias(true);
|
||||
|
||||
int outerSize = size + 18;
|
||||
canvas.drawCircle(centerX, centerY, outerSize / 2, outerOutlinePaint);
|
||||
|
||||
// Рисуем внутреннюю обводку
|
||||
Paint innerOutlinePaint = new Paint();
|
||||
innerOutlinePaint.setColor(0xFFFFFFFF);
|
||||
innerOutlinePaint.setStyle(Paint.Style.STROKE);
|
||||
innerOutlinePaint.setStrokeWidth(2f);
|
||||
innerOutlinePaint.setAntiAlias(true);
|
||||
|
||||
int innerSize = size + 16;
|
||||
canvas.drawCircle(centerX, centerY, innerSize / 2, innerOutlinePaint);
|
||||
|
||||
// Пробуем использовать иконку chosentarget если доступна
|
||||
int iconResId = context.getResources().getIdentifier("chosentarget", "drawable", context.getPackageName());
|
||||
if (iconResId != 0) {
|
||||
Drawable selectionDrawable = context.getResources().getDrawable(iconResId, null);
|
||||
if (selectionDrawable != null) {
|
||||
int selectionSize = size + 16;
|
||||
int selectionLeft = centerX - selectionSize / 2;
|
||||
int selectionTop = centerY - selectionSize / 2;
|
||||
|
||||
selectionDrawable.setBounds(selectionLeft, selectionTop,
|
||||
selectionLeft + selectionSize, selectionTop + selectionSize);
|
||||
selectionDrawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Игнорируем ошибки рамки выделения
|
||||
}
|
||||
}
|
||||
|
||||
private int getVesselColor() {
|
||||
if (aisVessel == null) return android.graphics.Color.WHITE;
|
||||
|
||||
String navStatus = aisVessel.getNavigationalStatus();
|
||||
if (navStatus != null) {
|
||||
switch (navStatus.toLowerCase()) {
|
||||
case "under way using engine":
|
||||
case "under way":
|
||||
return android.graphics.Color.GREEN;
|
||||
case "at anchor":
|
||||
return android.graphics.Color.YELLOW;
|
||||
case "moored":
|
||||
return android.graphics.Color.BLUE;
|
||||
case "not under command":
|
||||
case "restricted manoeuvrability":
|
||||
return android.graphics.Color.RED;
|
||||
default:
|
||||
return android.graphics.Color.WHITE;
|
||||
}
|
||||
}
|
||||
return android.graphics.Color.WHITE;
|
||||
}
|
||||
|
||||
public Vessel getVessel() {
|
||||
return vessel;
|
||||
}
|
||||
|
||||
public AISVessel getAISVessel() {
|
||||
return aisVessel;
|
||||
}
|
||||
|
||||
public boolean isOwnVessel() {
|
||||
return isOwnVessel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, устарели ли данные судна (для AIS судов)
|
||||
*/
|
||||
public boolean isDataStale() {
|
||||
if (isOwnVessel || aisVessel == null || settingsManager == null) {
|
||||
return false; // Собственное судно никогда не устаревает
|
||||
}
|
||||
return aisVessel.isDataStale(settingsManager.getDataStaleWarningMinutes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли удалить судно (для AIS судов)
|
||||
*/
|
||||
public boolean shouldBeRemoved() {
|
||||
if (isOwnVessel || aisVessel == null || settingsManager == null) {
|
||||
return false; // Собственное судно никогда не удаляется
|
||||
}
|
||||
return aisVessel.shouldBeRemoved(settingsManager.getDataStaleRemoveMinutes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает текущий зум карты
|
||||
*/
|
||||
private float getCurrentZoom() {
|
||||
try {
|
||||
if (mapView != null) {
|
||||
com.yandex.mapkit.map.CameraPosition cameraPosition = mapView.getMap().getCameraPosition();
|
||||
return cameraPosition.getZoom();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Ошибка получения зума, возвращаем значение по умолчанию
|
||||
}
|
||||
return 10f; // Значение по умолчанию
|
||||
}
|
||||
|
||||
/**
|
||||
* Рассчитывает размер маркера на основе зума и размеров судна
|
||||
*/
|
||||
private float calculateMarkerSize(float zoom) {
|
||||
// На очень большом расстоянии используем минимальный размер
|
||||
if (zoom < 8) {
|
||||
return MIN_MARKER_SIZE;
|
||||
}
|
||||
|
||||
// На среднем расстоянии используем средний размер
|
||||
if (zoom < 12) {
|
||||
return MEDIUM_ZOOM_SIZE;
|
||||
}
|
||||
|
||||
// На близком расстоянии используем крупный размер
|
||||
if (zoom < 15) {
|
||||
return CLOSE_ZOOM_SIZE;
|
||||
}
|
||||
|
||||
// При очень близком приближении рассчитываем размер на основе реальных размеров судна
|
||||
double vesselLength = 0;
|
||||
double vesselWidth = 0;
|
||||
|
||||
if (isOwnVessel && vessel != null) {
|
||||
// Для собственного судна используем примерные размеры
|
||||
vesselLength = 50; // метры
|
||||
vesselWidth = 10; // метры
|
||||
} else if (!isOwnVessel && aisVessel != null) {
|
||||
vesselLength = aisVessel.getLength();
|
||||
vesselWidth = aisVessel.getWidth();
|
||||
}
|
||||
|
||||
// Если размеры не заданы или очень маленькие, используем увеличенный базовый размер
|
||||
if (vesselLength <= 0 || vesselWidth <= 0 || vesselLength < 10 || vesselWidth < 5) {
|
||||
// Используем размер, основанный на зуме, но увеличенный
|
||||
float baseSize = CLOSE_ZOOM_SIZE + (zoom - 15) * 8; // Увеличиваем размер с зумом
|
||||
return Math.max(CLOSE_ZOOM_SIZE, Math.min(MAX_MARKER_SIZE, baseSize));
|
||||
}
|
||||
|
||||
// Рассчитываем размер на основе большего из размеров судна
|
||||
double vesselSize = Math.max(vesselLength, vesselWidth);
|
||||
|
||||
// Коэффициент масштабирования (пиксели на метр при текущем зуме)
|
||||
// Чем больше зум, тем больше пикселей на метр
|
||||
float pixelsPerMeter = (float) (Math.pow(2, zoom - 12) * 1.0); // Увеличенный коэффициент
|
||||
|
||||
// Размер маркера в пикселях
|
||||
float calculatedSize = (float) (vesselSize * pixelsPerMeter);
|
||||
|
||||
// Ограничиваем размер маркера, но с более высоким минимумом
|
||||
float minSize = Math.max(CLOSE_ZOOM_SIZE, MIN_MARKER_SIZE);
|
||||
return Math.max(minSize, Math.min(MAX_MARKER_SIZE, calculatedSize));
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public class AISVessel {
|
||||
private double course; // курс в градусах (0-360)
|
||||
private double speed; // скорость в узлах
|
||||
private double heading; // направление движения в градусах
|
||||
private double rateOfTurn; // скорость поворота в градусах/минуту
|
||||
private double length; // длина судна в метрах
|
||||
private double width; // ширина судна в метрах
|
||||
private double draft; // осадка в метрах
|
||||
@@ -29,6 +30,7 @@ public class AISVessel {
|
||||
private boolean positionAccuracy; // точность позиции
|
||||
private String vesselClass; // класс судна (Class A, Class B, Extended Class B)
|
||||
private String vendorId; // идентификатор производителя оборудования
|
||||
private boolean selected; // выделено ли судно на карте
|
||||
|
||||
public AISVessel() {
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
@@ -71,6 +73,9 @@ public class AISVessel {
|
||||
public double getHeading() { return heading; }
|
||||
public void setHeading(double heading) { this.heading = heading; }
|
||||
|
||||
public double getRateOfTurn() { return rateOfTurn; }
|
||||
public void setRateOfTurn(double rateOfTurn) { this.rateOfTurn = rateOfTurn; }
|
||||
|
||||
public double getLength() { return length; }
|
||||
public void setLength(double length) { this.length = length; }
|
||||
|
||||
@@ -110,6 +115,9 @@ public class AISVessel {
|
||||
public String getVendorId() { return vendorId; }
|
||||
public void setVendorId(String vendorId) { this.vendorId = vendorId; }
|
||||
|
||||
public boolean isSelected() { return selected; }
|
||||
public void setSelected(boolean selected) { this.selected = selected; }
|
||||
|
||||
/**
|
||||
* Обновляет позицию и курс судна
|
||||
*/
|
||||
@@ -122,12 +130,47 @@ public class AISVessel {
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, не устарели ли данные (больше 10 минут)
|
||||
* Обновляет позицию, курс и скорость поворота судна
|
||||
*/
|
||||
public void updatePosition(double latitude, double longitude, double course, double speed, double rateOfTurn) {
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
this.course = course;
|
||||
this.speed = speed;
|
||||
this.rateOfTurn = rateOfTurn;
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, не устарели ли данные (больше 10 минут)
|
||||
* @deprecated Используйте isDataStale(int warningMinutes) для настраиваемого времени
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean isDataStale() {
|
||||
return LocalDateTime.now().minusMinutes(10).isAfter(lastUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, не устарели ли данные на указанное количество минут
|
||||
*/
|
||||
public boolean isDataStale(int warningMinutes) {
|
||||
return LocalDateTime.now().minusMinutes(warningMinutes).isAfter(lastUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли удалить данные (старше указанного количества минут)
|
||||
*/
|
||||
public boolean shouldBeRemoved(int removeMinutes) {
|
||||
return LocalDateTime.now().minusMinutes(removeMinutes).isAfter(lastUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает количество минут с последнего обновления
|
||||
*/
|
||||
public long getMinutesSinceLastUpdate() {
|
||||
return java.time.Duration.between(lastUpdate, LocalDateTime.now()).toMinutes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AISVessel{" +
|
||||
@@ -137,6 +180,7 @@ public class AISVessel {
|
||||
", lon=" + longitude +
|
||||
", course=" + course +
|
||||
", speed=" + speed +
|
||||
", rot=" + rateOfTurn +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.grigowashere.aismap.models;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* Модель точки пути судна
|
||||
* Содержит координаты, скорость и время прохождения
|
||||
*/
|
||||
public class VesselPathPoint implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private double longitude;
|
||||
private double latitude;
|
||||
private float speed; // скорость в узлах
|
||||
private long timestamp; // время в миллисекундах
|
||||
|
||||
public VesselPathPoint() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public VesselPathPoint(double longitude, double latitude, float speed) {
|
||||
this.longitude = longitude;
|
||||
this.latitude = latitude;
|
||||
this.speed = speed;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public VesselPathPoint(double longitude, double latitude, float speed, long timestamp) {
|
||||
this.longitude = longitude;
|
||||
this.latitude = latitude;
|
||||
this.speed = speed;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
// Геттеры и сеттеры
|
||||
public double getLongitude() {
|
||||
return longitude;
|
||||
}
|
||||
|
||||
public void setLongitude(double longitude) {
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
public double getLatitude() {
|
||||
return latitude;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
this.latitude = latitude;
|
||||
}
|
||||
|
||||
public float getSpeed() {
|
||||
return speed;
|
||||
}
|
||||
|
||||
public void setSpeed(float speed) {
|
||||
this.speed = speed;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет расстояние до другой точки в метрах
|
||||
*/
|
||||
public double distanceTo(VesselPathPoint other) {
|
||||
if (other == null) return 0;
|
||||
|
||||
final int R = 6371000; // радиус Земли в метрах
|
||||
double lat1Rad = Math.toRadians(this.latitude);
|
||||
double lat2Rad = Math.toRadians(other.latitude);
|
||||
double deltaLatRad = Math.toRadians(other.latitude - this.latitude);
|
||||
double deltaLonRad = Math.toRadians(other.longitude - this.longitude);
|
||||
|
||||
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
|
||||
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
|
||||
Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет время между точками в секундах
|
||||
*/
|
||||
public long timeDifferenceSeconds(VesselPathPoint other) {
|
||||
if (other == null) return 0;
|
||||
return Math.abs(this.timestamp - other.timestamp) / 1000;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("VesselPathPoint{lon=%.6f, lat=%.6f, speed=%.1f, time=%d}",
|
||||
longitude, latitude, speed, timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) return true;
|
||||
if (obj == null || getClass() != obj.getClass()) return false;
|
||||
|
||||
VesselPathPoint that = (VesselPathPoint) obj;
|
||||
|
||||
return Double.compare(that.longitude, longitude) == 0 &&
|
||||
Double.compare(that.latitude, latitude) == 0 &&
|
||||
Float.compare(that.speed, speed) == 0 &&
|
||||
timestamp == that.timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result;
|
||||
long temp;
|
||||
temp = Double.doubleToLongBits(longitude);
|
||||
result = (int) (temp ^ (temp >>> 32));
|
||||
temp = Double.doubleToLongBits(latitude);
|
||||
result = 31 * result + (int) (temp ^ (temp >>> 32));
|
||||
result = 31 * result + (speed != 0.0f ? Float.floatToIntBits(speed) : 0);
|
||||
result = 31 * result + (int) (timestamp ^ (timestamp >>> 32));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ public class CompassSensor implements SensorEventListener {
|
||||
private CompassListener compassListener;
|
||||
private boolean isListening = false;
|
||||
|
||||
// Диагностика
|
||||
private long lastLogTime = 0;
|
||||
|
||||
// Скользящий фильтр для сглаживания значений
|
||||
private static final int FILTER_SIZE = 60;
|
||||
private float[] azimuthBuffer = new float[FILTER_SIZE];
|
||||
@@ -85,6 +88,13 @@ public class CompassSensor implements SensorEventListener {
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
// Диагностика: логируем каждые 10 секунд
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastLogTime > 10000) {
|
||||
Log.d(TAG, "🧭 CompassSensor: onSensorChanged работает (тип: " + event.sensor.getType() + ")");
|
||||
lastLogTime = now;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -121,6 +131,12 @@ public class CompassSensor implements SensorEventListener {
|
||||
|
||||
// Уведомляем слушателя
|
||||
if (compassListener != null) {
|
||||
// Диагностика: логируем каждые 10 секунд
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastLogTime > 10000) {
|
||||
Log.d(TAG, "🧭 CompassSensor: onCompassChanged вызывается, azimuth=" + filteredAzimuth);
|
||||
lastLogTime = now;
|
||||
}
|
||||
compassListener.onCompassChanged(filteredAzimuth);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.grigowashere.aismap.services;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.grigowashere.aismap.MainActivity;
|
||||
import com.grigowashere.aismap.R;
|
||||
|
||||
public class AISForegroundService extends Service {
|
||||
|
||||
public static final String CHANNEL_ID = "aismap_foreground";
|
||||
private static final int NOTIFICATION_ID = 1001;
|
||||
|
||||
// Константы для действий
|
||||
public static final String ACTION_STOP_SERVICE = "com.grigowashere.aismap.STOP_SERVICE";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
android.util.Log.i("AISForegroundService", "onCreate() вызван");
|
||||
|
||||
try {
|
||||
createNotificationChannel();
|
||||
Notification notification = buildNotification("Работа в фоне: обновление AIS/GPS");
|
||||
android.util.Log.i("AISForegroundService", "Уведомление создано: " + notification);
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification);
|
||||
android.util.Log.i("AISForegroundService", "Сервис запущен в форграунд режиме");
|
||||
} catch (Exception e) {
|
||||
android.util.Log.e("AISForegroundService", "Ошибка при запуске сервиса: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null && ACTION_STOP_SERVICE.equals(intent.getAction())) {
|
||||
// Останавливаем сервис
|
||||
stopForeground(true);
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
// Здесь в дальнейшем запустим прием NMEA/UDP и GPS слушателей
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
// Останавливаем форграунд режим
|
||||
stopForeground(true);
|
||||
|
||||
// Здесь можно добавить очистку ресурсов, если они есть
|
||||
// Например, остановка GPS слушателей, UDP соединений и т.д.
|
||||
|
||||
android.util.Log.i("AISForegroundService", "Сервис остановлен");
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
android.util.Log.i("AISForegroundService", "Создание канала уведомлений...");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"AISMap Background",
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
);
|
||||
channel.setDescription("Фоновые обновления AIS и GPS");
|
||||
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (nm != null) {
|
||||
nm.createNotificationChannel(channel);
|
||||
android.util.Log.i("AISForegroundService", "Канал уведомлений создан: " + CHANNEL_ID);
|
||||
} else {
|
||||
android.util.Log.e("AISForegroundService", "NotificationManager равен null!");
|
||||
}
|
||||
} else {
|
||||
android.util.Log.i("AISForegroundService", "Android версия < O, канал не нужен");
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification(String content) {
|
||||
android.util.Log.i("AISForegroundService", "Создание уведомления с текстом: " + content);
|
||||
|
||||
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT : 0;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags);
|
||||
|
||||
// Создаем действие для остановки сервиса
|
||||
Intent stopIntent = new Intent(this, AISForegroundService.class);
|
||||
stopIntent.setAction(ACTION_STOP_SERVICE);
|
||||
PendingIntent stopPendingIntent = PendingIntent.getService(this, 0, stopIntent, flags);
|
||||
|
||||
android.util.Log.i("AISForegroundService", "Создание уведомления с кнопкой остановки");
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("AISMap")
|
||||
.setContentText(content)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Остановить", stopPendingIntent)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
package com.grigowashere.aismap.services;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.media.AudioManager;
|
||||
import android.media.ToneGenerator;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.os.VibratorManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
|
||||
import com.grigowashere.aismap.MainActivity;
|
||||
import com.grigowashere.aismap.R;
|
||||
import com.grigowashere.aismap.utils.SettingsManager;
|
||||
|
||||
/**
|
||||
* Сервис для обработки уведомлений о новых AIS целях
|
||||
* Поддерживает вибрацию и звуковые уведомления
|
||||
*/
|
||||
public class NotificationService {
|
||||
|
||||
private static final String TAG = "NotificationService";
|
||||
private static final String ALERT_CHANNEL_ID = "aismap_alerts";
|
||||
private static final int SAFETY_NOTIFICATION_ID_BASE = 2000;
|
||||
|
||||
private Context context;
|
||||
private SettingsManager settingsManager;
|
||||
private Vibrator vibrator;
|
||||
private ToneGenerator toneGenerator;
|
||||
private boolean isInitialized = false;
|
||||
|
||||
public NotificationService(Context context) {
|
||||
this.context = context;
|
||||
this.settingsManager = new SettingsManager(context);
|
||||
initializeService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует сервис уведомлений
|
||||
*/
|
||||
private void initializeService() {
|
||||
try {
|
||||
createAlertChannel();
|
||||
// Инициализация вибратора
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
|
||||
vibrator = vibratorManager.getDefaultVibrator();
|
||||
} else {
|
||||
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
// Инициализация генератора тонов
|
||||
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
|
||||
|
||||
isInitialized = true;
|
||||
Log.i(TAG, "Сервис уведомлений инициализирован успешно");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка инициализации сервиса уведомлений: " + e.getMessage(), e);
|
||||
isInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает канал уведомлений для предупреждений (Android O+)
|
||||
*/
|
||||
private void createAlertChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
ALERT_CHANNEL_ID,
|
||||
"AIS Alerts",
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
);
|
||||
channel.setDescription("Сообщения безопасности AIS и предупреждения");
|
||||
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (nm != null) nm.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Воспроизводит уведомление о новой AIS цели
|
||||
*/
|
||||
public void notifyNewAISTarget() {
|
||||
if (!isInitialized) {
|
||||
Log.w(TAG, "Сервис уведомлений не инициализирован");
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем настройки и воспроизводим соответствующие уведомления
|
||||
if (settingsManager.isVibrationEnabled()) {
|
||||
playVibration();
|
||||
}
|
||||
|
||||
if (settingsManager.isSoundEnabled()) {
|
||||
playSound();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Уведомление о новой AIS цели воспроизведено");
|
||||
}
|
||||
|
||||
/**
|
||||
* Воспроизводит вибрацию
|
||||
*/
|
||||
private void playVibration() {
|
||||
try {
|
||||
if (vibrator != null && vibrator.hasVibrator()) {
|
||||
// Паттерн вибрации: короткая пауза, длинная вибрация, короткая пауза, короткая вибрация
|
||||
long[] pattern = {0, 200, 100, 100};
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||
vibrator.vibrate(effect);
|
||||
} else {
|
||||
vibrator.vibrate(pattern, -1);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Вибрация воспроизведена");
|
||||
} else {
|
||||
Log.w(TAG, "Вибратор недоступен");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка воспроизведения вибрации: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Воспроизводит звуковое уведомление
|
||||
*/
|
||||
private void playSound() {
|
||||
try {
|
||||
if (toneGenerator != null) {
|
||||
// Воспроизводим тон уведомления (TONE_CDMA_ALERT_CALL_GUARD)
|
||||
toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 500);
|
||||
|
||||
Log.d(TAG, "Звуковое уведомление воспроизведено");
|
||||
} else {
|
||||
Log.w(TAG, "Генератор тонов недоступен");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка воспроизведения звука: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включены ли уведомления
|
||||
*/
|
||||
public boolean areNotificationsEnabled() {
|
||||
return settingsManager.isVibrationEnabled() || settingsManager.isSoundEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включена ли вибрация
|
||||
*/
|
||||
public boolean isVibrationEnabled() {
|
||||
return settingsManager.isVibrationEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли звук
|
||||
*/
|
||||
public boolean isSoundEnabled() {
|
||||
return settingsManager.isSoundEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомление о сообщении безопасности (AIS 14)
|
||||
*/
|
||||
public void notifySafetyMessage(String mmsi, String text) {
|
||||
if (!isInitialized) {
|
||||
Log.w(TAG, "Сервис уведомлений не инициализирован");
|
||||
return;
|
||||
}
|
||||
// Подаем сигнал по настройкам (вибро/звук)
|
||||
if (settingsManager.isVibrationEnabled()) {
|
||||
playVibration();
|
||||
}
|
||||
if (settingsManager.isSoundEnabled()) {
|
||||
playSound();
|
||||
}
|
||||
|
||||
// Показ системного уведомления в шторке
|
||||
try {
|
||||
createAlertChannel();
|
||||
Intent intent = new Intent(context, MainActivity.class);
|
||||
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT : 0;
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flags);
|
||||
|
||||
String title = "AIS Safety message";
|
||||
String content = (text != null && !text.isEmpty()) ? text : ("Сообщение от " + mmsi);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ALERT_CHANNEL_ID)
|
||||
.setSmallIcon(R.mipmap.ic_launcher)
|
||||
.setContentTitle(title)
|
||||
.setContentText(content)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(content))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true);
|
||||
|
||||
int notificationId = SAFETY_NOTIFICATION_ID_BASE + (mmsi != null ? (mmsi.hashCode() & 0x0FFF) : 0);
|
||||
NotificationManagerCompat.from(context).notify(notificationId, builder.build());
|
||||
|
||||
Log.i(TAG, "Показано системное уведомление о safety-сообщении: MMSI=" + mmsi);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка показа системного уведомления: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Освобождает ресурсы сервиса
|
||||
*/
|
||||
public void cleanup() {
|
||||
try {
|
||||
if (toneGenerator != null) {
|
||||
toneGenerator.release();
|
||||
toneGenerator = null;
|
||||
}
|
||||
|
||||
if (vibrator != null) {
|
||||
vibrator.cancel();
|
||||
}
|
||||
|
||||
isInitialized = false;
|
||||
Log.i(TAG, "Ресурсы сервиса уведомлений освобождены");
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка при освобождении ресурсов сервиса уведомлений: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.grigowashere.aismap.ui;
|
||||
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
/**
|
||||
* Интерфейс для уведомлений UI о изменениях данных
|
||||
* Контроллеры используют этот интерфейс для информирования UI о изменениях
|
||||
* без знания деталей UI реализации
|
||||
*/
|
||||
public interface UIDataChangeNotifier {
|
||||
|
||||
/**
|
||||
* Уведомление об изменении позиции собственного судна
|
||||
* @param vessel обновленные данные судна
|
||||
*/
|
||||
void onVesselPositionChanged(Vessel vessel);
|
||||
|
||||
/**
|
||||
* Уведомление об изменении качества GPS данных
|
||||
* @param vessel данные судна с обновленными GPS метаданными
|
||||
*/
|
||||
void onGPSQualityChanged(Vessel vessel);
|
||||
|
||||
/**
|
||||
* Уведомление о новой AIS судне или обновлении существующего
|
||||
* @param vessel данные AIS судна
|
||||
*/
|
||||
void onAISVesselChanged(AISVessel vessel);
|
||||
|
||||
/**
|
||||
* Уведомление об удалении AIS судна
|
||||
* @param mmsi идентификатор удаляемого судна
|
||||
*/
|
||||
void onAISVesselRemoved(String mmsi);
|
||||
|
||||
/**
|
||||
* Уведомление об изменении пути судна
|
||||
* @param mmsi идентификатор судна (null для собственного судна)
|
||||
*/
|
||||
void onVesselPathChanged(String mmsi);
|
||||
|
||||
/**
|
||||
* Уведомление о центрировании карты
|
||||
* @param latitude широта
|
||||
* @param longitude долгота
|
||||
*/
|
||||
void onRequestCenterMap(double latitude, double longitude);
|
||||
|
||||
/**
|
||||
* Уведомление об обновлении компаса
|
||||
* @param azimuth значение азимута
|
||||
*/
|
||||
void onCompassUpdate(float azimuth);
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package com.grigowashere.aismap.ui;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import com.grigowashere.aismap.maps.MapInterface;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* Координатор UI отрисовки
|
||||
* Единая точка всех операций с картой и UI
|
||||
* Обеспечивает throttling и батчинг операций
|
||||
*/
|
||||
public class UIRenderingCoordinator implements UIDataChangeNotifier {
|
||||
private static final String TAG = "UIRenderingCoordinator";
|
||||
|
||||
// Throttling интервалы
|
||||
public static final long VESSEL_UPDATE_THROTTLE = 500; // 500мс для позиции судна
|
||||
public static final long AIS_UPDATE_THROTTLE = 1000; // 1сек для AIS данных
|
||||
public static final long PATH_UPDATE_THROTTLE = 2000; // 2сек для путей
|
||||
|
||||
private MapInterface mapInterface;
|
||||
private Handler uiHandler;
|
||||
|
||||
// Pending операции для батчинга
|
||||
private Vessel pendingVesselUpdate;
|
||||
private final Map<String, AISVessel> pendingAISUpdates = new HashMap<>();
|
||||
private final Set<String> pendingAISRemovals = new HashSet<>();
|
||||
|
||||
// Throttling Runnable's
|
||||
private Runnable vesselUpdateRunnable;
|
||||
private Runnable aisUpdateRunnable;
|
||||
private Runnable pathUpdateRunnable;
|
||||
|
||||
// Флаги для предотвращения множественных запланированных операций
|
||||
private boolean vesselUpdatePending = false;
|
||||
private boolean aisUpdatePending = false;
|
||||
private boolean pathUpdatePending = false;
|
||||
|
||||
public UIRenderingCoordinator(MapInterface mapInterface) {
|
||||
this.mapInterface = mapInterface;
|
||||
this.uiHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
setupThrottling();
|
||||
Log.i(TAG, "UIRenderingCoordinator инициализирован");
|
||||
}
|
||||
|
||||
/**
|
||||
* Настройка throttling механизмов
|
||||
*/
|
||||
private void setupThrottling() {
|
||||
vesselUpdateRunnable = () -> {
|
||||
vesselUpdatePending = false;
|
||||
executeVesselUpdate();
|
||||
};
|
||||
|
||||
aisUpdateRunnable = () -> {
|
||||
aisUpdatePending = false;
|
||||
executeAISUpdates();
|
||||
};
|
||||
|
||||
pathUpdateRunnable = () -> {
|
||||
pathUpdatePending = false;
|
||||
executePathUpdates();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрос обновления позиции собственного судна
|
||||
*/
|
||||
public void requestVesselUpdate(Vessel vessel) {
|
||||
if (vessel == null) return;
|
||||
|
||||
pendingVesselUpdate = vessel;
|
||||
|
||||
if (!vesselUpdatePending) {
|
||||
vesselUpdatePending = true;
|
||||
uiHandler.removeCallbacks(vesselUpdateRunnable);
|
||||
uiHandler.postDelayed(vesselUpdateRunnable, VESSEL_UPDATE_THROTTLE);
|
||||
|
||||
Log.d(TAG, "Vessel update запланирован на " + VESSEL_UPDATE_THROTTLE + "мс");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрос обновления AIS судна
|
||||
*/
|
||||
public void requestAISUpdate(AISVessel vessel) {
|
||||
if (vessel == null || vessel.getMmsi() == null) return;
|
||||
|
||||
pendingAISUpdates.put(vessel.getMmsi(), vessel);
|
||||
|
||||
if (!aisUpdatePending) {
|
||||
aisUpdatePending = true;
|
||||
uiHandler.removeCallbacks(aisUpdateRunnable);
|
||||
uiHandler.postDelayed(aisUpdateRunnable, AIS_UPDATE_THROTTLE);
|
||||
|
||||
Log.d(TAG, "AIS update запланирован на " + AIS_UPDATE_THROTTLE + "мс");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Запрос удаления AIS судна
|
||||
*/
|
||||
public void requestAISRemoval(String mmsi) {
|
||||
if (mmsi == null) return;
|
||||
|
||||
pendingAISRemovals.add(mmsi);
|
||||
pendingAISUpdates.remove(mmsi); // Убираем из обновлений
|
||||
|
||||
if (!aisUpdatePending) {
|
||||
aisUpdatePending = true;
|
||||
uiHandler.removeCallbacks(aisUpdateRunnable);
|
||||
uiHandler.postDelayed(aisUpdateRunnable, AIS_UPDATE_THROTTLE);
|
||||
|
||||
Log.d(TAG, "AIS removal запланирован на " + AIS_UPDATE_THROTTLE + "мс");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнение обновления позиции судна
|
||||
*/
|
||||
private void executeVesselUpdate() {
|
||||
if (mapInterface == null || pendingVesselUpdate == null) return;
|
||||
|
||||
try {
|
||||
Log.d(TAG, "Выполняем vessel update: " + pendingVesselUpdate.getLatitude() + "," + pendingVesselUpdate.getLongitude());
|
||||
mapInterface.updateOwnVesselPosition(pendingVesselUpdate);
|
||||
Log.d(TAG, "Vessel update выполнен успешно");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка vessel update: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
pendingVesselUpdate = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнение обновлений AIS судов
|
||||
*/
|
||||
private void executeAISUpdates() {
|
||||
if (mapInterface == null) return;
|
||||
|
||||
try {
|
||||
// Удаляем старые суда
|
||||
for (String mmsi : pendingAISRemovals) {
|
||||
Log.d(TAG, "Удаляем AIS судно: " + mmsi);
|
||||
mapInterface.removeAISVesselMarker(mmsi);
|
||||
}
|
||||
|
||||
// Обновляем или добавляем суда (различать не будем - MapInterface сам решит)
|
||||
for (AISVessel vessel : pendingAISUpdates.values()) {
|
||||
Log.d(TAG, "Обновляем/добавляем AIS судно: " + vessel.getMmsi());
|
||||
mapInterface.updateAISVesselPosition(vessel);
|
||||
}
|
||||
|
||||
Log.d(TAG, "AIS updates выполнены: удалено=" + pendingAISRemovals.size() +
|
||||
", обновлено=" + pendingAISUpdates.size());
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка AIS updates: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// Очищаем pending операции
|
||||
pendingAISUpdates.clear();
|
||||
pendingAISRemovals.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Выполнение обновлений путей (заглушка для будущего)
|
||||
*/
|
||||
private void executePathUpdates() {
|
||||
if (mapInterface == null) return;
|
||||
|
||||
try {
|
||||
// TODO: Реализовать батчинговое обновление путей
|
||||
Log.d(TAG, "Path updates выполнены (заглушка)");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка path updates: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительное выполнение всех pending операций
|
||||
*/
|
||||
public void flushPendingOperations() {
|
||||
Log.i(TAG, "Принудительное выполнение всех pending операций");
|
||||
|
||||
if (uiHandler != null) {
|
||||
uiHandler.removeCallbacks(vesselUpdateRunnable);
|
||||
uiHandler.removeCallbacks(aisUpdateRunnable);
|
||||
uiHandler.removeCallbacks(pathUpdateRunnable);
|
||||
}
|
||||
|
||||
vesselUpdatePending = false;
|
||||
aisUpdatePending = false;
|
||||
pathUpdatePending = false;
|
||||
|
||||
executeVesselUpdate();
|
||||
executeAISUpdates();
|
||||
executePathUpdates();
|
||||
|
||||
Log.i(TAG, "Все pending операции выполнены");
|
||||
}
|
||||
|
||||
/**
|
||||
* Очистка ресурсов
|
||||
*/
|
||||
public void cleanup() {
|
||||
Log.i(TAG, "Очистка UIRenderingCoordinator");
|
||||
|
||||
if (uiHandler != null) {
|
||||
uiHandler.removeCallbacksAndMessages(null);
|
||||
}
|
||||
|
||||
flushPendingOperations();
|
||||
mapInterface = null;
|
||||
|
||||
Log.i(TAG, "UIRenderingCoordinator очищен");
|
||||
}
|
||||
|
||||
// ========== Реализация UIDataChangeNotifier ==========
|
||||
|
||||
@Override
|
||||
public void onVesselPositionChanged(Vessel vessel) {
|
||||
requestVesselUpdate(vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGPSQualityChanged(Vessel vessel) {
|
||||
// GPS качество влияет на отображение точности, но не требует urgent update
|
||||
requestVesselUpdate(vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAISVesselChanged(AISVessel vessel) {
|
||||
requestAISUpdate(vessel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAISVesselRemoved(String mmsi) {
|
||||
requestAISRemoval(mmsi);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVesselPathChanged(String mmsi) {
|
||||
// Path изменения менее критичны, используем больше throttling
|
||||
if (!pathUpdatePending) {
|
||||
pathUpdatePending = true;
|
||||
uiHandler.removeCallbacks(pathUpdateRunnable);
|
||||
uiHandler.postDelayed(pathUpdateRunnable, PATH_UPDATE_THROTTLE);
|
||||
|
||||
Log.d(TAG, "Path update запланирован на " + PATH_UPDATE_THROTTLE + "мс");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestCenterMap(double latitude, double longitude) {
|
||||
// Центрирование карты должно происходить немедленно
|
||||
uiHandler.post(() -> {
|
||||
if (mapInterface != null) {
|
||||
mapInterface.centerOnPosition(latitude, longitude);
|
||||
Log.d(TAG, "Карта отцентрирована на " + latitude + "," + longitude);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompassUpdate(float azimuth) {
|
||||
// Компас не относится к карте, передаем в MainActivity через callback
|
||||
Log.d(TAG, "Compass update: " + azimuth + "° - требует специальной обработки в MainActivity");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
package com.grigowashere.aismap.utils;
|
||||
|
||||
import android.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* Утилита для отправки логов на внешний ресурс
|
||||
* Отправляет GET запросы на https://ais.grigowashere.ru/add
|
||||
*/
|
||||
public class LogSender {
|
||||
|
||||
private static final String TAG = "LogSender";
|
||||
private static final String BASE_URL = "https://ais.grigowashere.ru/add";
|
||||
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
/**
|
||||
* Отправляет лог NMEA сообщения
|
||||
* @param nmeaMessage NMEA сообщение
|
||||
*/
|
||||
public static void logNMEA(String nmeaMessage) {
|
||||
if (nmeaMessage == null || nmeaMessage.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String encodedMessage = encodeForURL(nmeaMessage);
|
||||
String url = BASE_URL + "?nmea=" + encodedMessage + "&color=blue";
|
||||
|
||||
sendGetRequest(url);
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "NMEA лог отправлен: " + nmeaMessage);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка отправки NMEA лога: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет лог обновления информации о корабле
|
||||
* @param mmsi MMSI корабля
|
||||
* @param vesselInfo Информация о корабле
|
||||
*/
|
||||
public static void logShipUpdate(String mmsi, String vesselInfo) {
|
||||
if (mmsi == null || mmsi.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String message = "MMSI: " + mmsi;
|
||||
if (vesselInfo != null && !vesselInfo.trim().isEmpty()) {
|
||||
message += " | " + vesselInfo;
|
||||
}
|
||||
|
||||
// Извлекаем тип судна из vesselInfo и генерируем цвет
|
||||
// Генерируем уникальный цвет для корабля на основе MMSI
|
||||
String vesselColor = generateVesselColor(mmsi);
|
||||
|
||||
String encodedMessage = encodeForURL(message);
|
||||
String encodedColor = encodeColorForURL(vesselColor);
|
||||
String url = BASE_URL + "?ships=" + encodedMessage + "&color=" + encodedColor;
|
||||
|
||||
sendGetRequest(url);
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "Ship update лог отправлен: " + message + " ( " + ", цвет: " + vesselColor + ")");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка отправки ship update лога: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет лог обновления информации о корабле с заданным цветом
|
||||
* @param mmsi MMSI корабля
|
||||
* @param vesselInfo Информация о корабле
|
||||
* @param color Цвет в формате HEX (#RRGGBB) или имя цвета
|
||||
*/
|
||||
public static void logShipUpdate(String mmsi, String vesselInfo, String color) {
|
||||
if (mmsi == null || mmsi.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String message = "MMSI: " + mmsi;
|
||||
if (vesselInfo != null && !vesselInfo.trim().isEmpty()) {
|
||||
message += " | " + vesselInfo;
|
||||
}
|
||||
|
||||
// Используем переданный цвет или генерируем на основе типа судна
|
||||
String vesselColor;
|
||||
if (color != null && !color.trim().isEmpty()) {
|
||||
vesselColor = color;
|
||||
} else {
|
||||
// Генерируем уникальный цвет для корабля на основе MMSI
|
||||
vesselColor = generateVesselColor(mmsi);
|
||||
}
|
||||
|
||||
String encodedMessage = encodeForURL(message);
|
||||
String encodedColor = encodeColorForURL(vesselColor);
|
||||
String url = BASE_URL + "?ships=" + encodedMessage + "&color=" + encodedColor;
|
||||
|
||||
sendGetRequest(url);
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "Ship update лог отправлен: " + message + " (цвет: " + vesselColor + ")");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка отправки ship update лога: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет произвольный лог
|
||||
* @param logName Имя лога
|
||||
* @param message Сообщение
|
||||
* @param color Цвет (опционально)
|
||||
*/
|
||||
public static void logCustom(String logName, String message, String color) {
|
||||
if (logName == null || logName.trim().isEmpty() || message == null || message.trim().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
String encodedMessage = encodeForURL(message);
|
||||
String url = BASE_URL + "?" + logName + "=" + encodedMessage;
|
||||
|
||||
if (color != null && !color.trim().isEmpty()) {
|
||||
url += "&color=" + color;
|
||||
}
|
||||
|
||||
sendGetRequest(url);
|
||||
Log.d(TAG, "Custom лог отправлен: " + logName + " = " + message);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка отправки custom лога: " + e.getMessage(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Генерирует уникальный цвет для корабля на основе MMSI (устаревший метод)
|
||||
* @param mmsi MMSI корабля
|
||||
* @return HEX цвет в формате #RRGGBB
|
||||
*/
|
||||
private static String generateVesselColor(String mmsi) {
|
||||
try {
|
||||
// Преобразуем MMSI в число для хеширования
|
||||
long mmsiValue = Long.parseLong(mmsi);
|
||||
|
||||
// Используем хеш-функцию для получения равномерного распределения
|
||||
int hash = Long.hashCode(mmsiValue);
|
||||
|
||||
// Извлекаем RGB компоненты из хеша
|
||||
int r = (hash & 0xFF0000) >> 16;
|
||||
int g = (hash & 0x00FF00) >> 8;
|
||||
int b = hash & 0x0000FF;
|
||||
|
||||
// Проверяем, не слишком ли темный цвет (чтобы избежать черного)
|
||||
int brightness = (r + g + b) / 3;
|
||||
if (brightness < 100) {
|
||||
// Если цвет слишком темный, осветляем его
|
||||
r = Math.min(255, r + 120);
|
||||
g = Math.min(255, g + 120);
|
||||
b = Math.min(255, b + 120);
|
||||
}
|
||||
|
||||
// Проверяем, не слишком ли светлый цвет (чтобы избежать белого)
|
||||
if (brightness > 220) {
|
||||
// Если цвет слишком светлый, затемняем его
|
||||
r = Math.max(0, r - 60);
|
||||
g = Math.max(0, g - 60);
|
||||
b = Math.max(0, b - 60);
|
||||
}
|
||||
|
||||
// Форматируем в HEX
|
||||
String color = String.format("#%02X%02X%02X", r, g, b);
|
||||
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "Сгенерирован цвет для MMSI " + mmsi + ": " + color + " (RGB: " + r + "," + g + "," + b + ")");
|
||||
|
||||
return color;
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
Log.w(TAG, "Не удалось распарсить MMSI как число: " + mmsi + ", используем цвет по умолчанию");
|
||||
return "#00AA00"; // Зеленый по умолчанию
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка генерации цвета для MMSI " + mmsi + ": " + e.getMessage(), e);
|
||||
return "#00AA00"; // Зеленый по умолчанию
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Определяет тип судна по MMSI
|
||||
* Использует более точную логику на основе стандартных диапазонов MMSI
|
||||
* @param mmsi MMSI судна
|
||||
* @return Тип судна
|
||||
*/
|
||||
private static String getVesselTypeByMMSI(long mmsi) {
|
||||
// Стандартные диапазоны MMSI для разных типов судов
|
||||
if (mmsi >= 100000000 && mmsi <= 199999999) {
|
||||
return "COASTAL"; // Прибрежные суда
|
||||
} else if (mmsi >= 200000000 && mmsi <= 299999999) {
|
||||
return "FISHING"; // Рыболовные суда
|
||||
} else if (mmsi >= 300000000 && mmsi <= 399999999) {
|
||||
return "CARGO"; // Грузовые суда
|
||||
} else if (mmsi >= 400000000 && mmsi <= 499999999) {
|
||||
return "TANKER"; // Танкеры
|
||||
} else if (mmsi >= 500000000 && mmsi <= 599999999) {
|
||||
return "PASSENGER"; // Пассажирские суда
|
||||
} else if (mmsi >= 600000000 && mmsi <= 699999999) {
|
||||
return "MILITARY"; // Военные корабли
|
||||
} else if (mmsi >= 700000000 && mmsi <= 799999999) {
|
||||
return "PILOT"; // Лоцманские суда
|
||||
} else if (mmsi >= 800000000 && mmsi <= 899999999) {
|
||||
return "PILOT"; // Лоцманские суда (дополнительный диапазон)
|
||||
} else if (mmsi >= 900000000 && mmsi <= 999999999) {
|
||||
return "MILITARY"; // Военные корабли (дополнительный диапазон)
|
||||
} else if (mmsi >= 1000000000 && mmsi <= 1099999999) {
|
||||
return "SAR"; // Спасательные суда
|
||||
} else if (mmsi >= 1100000000 && mmsi <= 1199999999) {
|
||||
return "TUG"; // Буксиры
|
||||
} else if (mmsi >= 1200000000 && mmsi <= 1299999999) {
|
||||
return "PORT_TENDER"; // Портовые суда
|
||||
} else if (mmsi >= 1300000000 && mmsi <= 1399999999) {
|
||||
return "ANTI_POLLUTION"; // Антизагрязнительные суда
|
||||
} else if (mmsi >= 1400000000 && mmsi <= 1499999999) {
|
||||
return "LAW_ENFORCEMENT"; // Правоохранительные суда
|
||||
} else if (mmsi >= 1500000000 && mmsi <= 1599999999) {
|
||||
return "MEDICAL"; // Медицинские суда
|
||||
} else if (mmsi >= 1600000000 && mmsi <= 1699999999) {
|
||||
return "SPECIAL_CRAFT"; // Специальные суда
|
||||
} else if (mmsi >= 1700000000 && mmsi <= 1799999999) {
|
||||
return "PASSENGER"; // Пассажирские суда (дополнительный диапазон)
|
||||
} else if (mmsi >= 1800000000 && mmsi <= 1899999999) {
|
||||
return "CARGO"; // Грузовые суда (дополнительный диапазон)
|
||||
} else if (mmsi >= 1900000000 && mmsi <= 1999999999) {
|
||||
return "TANKER"; // Танкеры (дополнительный диапазон)
|
||||
} else if (mmsi >= 2000000000 && mmsi <= 2099999999) {
|
||||
return "OTHER"; // Другие типы судов
|
||||
} else if (mmsi >= 2100000000L && mmsi <= 2199999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2200000000L && mmsi <= 2299999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2300000000L && mmsi <= 2399999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2400000000L && mmsi <= 2499999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2500000000L && mmsi <= 2599999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2600000000L && mmsi <= 2699999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2700000000L && mmsi <= 2799999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2800000000L && mmsi <= 2899999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else if (mmsi >= 2900000000L && mmsi <= 2999999999L) {
|
||||
return "OTHER"; // Другие типы судов (дополнительный диапазон)
|
||||
} else {
|
||||
return "UNKNOWN"; // Неизвестный тип
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Кодирует цвет для безопасного использования в URL
|
||||
* Специально обрабатывает HEX цвета, заменяя # на %23
|
||||
* @param color Цвет в формате HEX (#RRGGBB) или имя цвета
|
||||
* @return Закодированный цвет
|
||||
*/
|
||||
private static String encodeColorForURL(String color) {
|
||||
if (color == null || color.trim().isEmpty()) {
|
||||
return "green"; // Цвет по умолчанию
|
||||
}
|
||||
|
||||
try {
|
||||
// Если цвет начинается с #, заменяем его на %23
|
||||
if (color.startsWith("#")) {
|
||||
String encoded = "%23" + color.substring(1);
|
||||
Log.d(TAG, "Закодирован HEX цвет: " + color + " -> " + encoded);
|
||||
return encoded;
|
||||
} else {
|
||||
// Для именованных цветов используем стандартное кодирование
|
||||
String encoded = URLEncoder.encode(color, StandardCharsets.UTF_8.toString());
|
||||
Log.d(TAG, "Закодирован именованный цвет: " + color + " -> " + encoded);
|
||||
return encoded;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка кодирования цвета: " + e.getMessage(), e);
|
||||
return "green"; // Цвет по умолчанию
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Кодирует строку для безопасного использования в URL
|
||||
* Дополнительно экранирует символы, которые могут вызывать проблемы
|
||||
* @param message Исходное сообщение
|
||||
* @return Закодированное сообщение
|
||||
*/
|
||||
private static String encodeForURL(String message) {
|
||||
try {
|
||||
// Сначала используем стандартное URL кодирование
|
||||
String encoded = URLEncoder.encode(message, StandardCharsets.UTF_8.toString());
|
||||
|
||||
// Дополнительно экранируем символы, которые могут вызывать проблемы
|
||||
// Заменяем < на %3C, > на %3E, & на %26, " на %22, ' на %27, # на %23
|
||||
encoded = encoded.replace("<", "%3C")
|
||||
.replace(">", "%3E")
|
||||
.replace("&", "%26")
|
||||
.replace("\"", "%22")
|
||||
.replace("'", "%27")
|
||||
.replace("#", "%23");
|
||||
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "Исходное сообщение: " + message);
|
||||
// Log.d(TAG, "Закодированное сообщение: " + encoded);
|
||||
|
||||
return encoded;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Ошибка кодирования URL: " + e.getMessage(), e);
|
||||
// В случае ошибки возвращаем базовое кодирование
|
||||
String fallback = message.replace("<", "%3C")
|
||||
.replace(">", "%3E")
|
||||
.replace("&", "%26")
|
||||
.replace("\"", "%22")
|
||||
.replace("'", "%27")
|
||||
.replace("#", "%23")
|
||||
.replace(" ", "%20");
|
||||
Log.d(TAG, "Fallback кодирование: " + fallback);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Отправляет GET запрос
|
||||
* @param urlString URL для запроса
|
||||
*/
|
||||
private static void sendGetRequest(String urlString) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "Отправляем GET запрос на: " + urlString);
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
URL url = new URL(urlString);
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(5000); // 5 секунд
|
||||
connection.setReadTimeout(5000); // 5 секунд
|
||||
connection.setRequestProperty("User-Agent", "AISMap/1.0");
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||
// Убираем лишние логи
|
||||
// Log.d(TAG, "Лог успешно отправлен, код ответа: " + responseCode);
|
||||
} else {
|
||||
Log.w(TAG, "Лог отправлен с предупреждением, код ответа: " + responseCode);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Ошибка HTTP запроса: " + e.getMessage(), e);
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Останавливает executor (вызывать при завершении приложения)
|
||||
*/
|
||||
public static void shutdown() {
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.grigowashere.aismap.utils;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Маппинг MID (первые 3 цифры MMSI) -> ISO 3166-1 alpha-2 кода страны
|
||||
*/
|
||||
public final class MIDToCountry {
|
||||
|
||||
public static final Map<String, String> MID_TO_COUNTRY;
|
||||
|
||||
static {
|
||||
MID_TO_COUNTRY = new HashMap<>();
|
||||
// Europe
|
||||
MID_TO_COUNTRY.put("201", "AL"); // Albania
|
||||
MID_TO_COUNTRY.put("202", "AD"); // Andorra
|
||||
MID_TO_COUNTRY.put("203", "AT"); // Austria
|
||||
MID_TO_COUNTRY.put("204", "PT"); // Portugal (Azores)
|
||||
MID_TO_COUNTRY.put("205", "BE"); // Belgium
|
||||
MID_TO_COUNTRY.put("206", "BY"); // Belarus
|
||||
MID_TO_COUNTRY.put("207", "BG"); // Bulgaria
|
||||
MID_TO_COUNTRY.put("208", "VA"); // Vatican City
|
||||
MID_TO_COUNTRY.put("209", "CY"); // Cyprus
|
||||
MID_TO_COUNTRY.put("210", "CY"); // Cyprus
|
||||
MID_TO_COUNTRY.put("211", "DE"); // Germany
|
||||
MID_TO_COUNTRY.put("212", "CY"); // Cyprus
|
||||
MID_TO_COUNTRY.put("213", "GE"); // Georgia
|
||||
MID_TO_COUNTRY.put("214", "MD"); // Moldova
|
||||
MID_TO_COUNTRY.put("215", "MT"); // Malta
|
||||
MID_TO_COUNTRY.put("216", "AM"); // Armenia
|
||||
MID_TO_COUNTRY.put("218", "DE"); // Germany
|
||||
MID_TO_COUNTRY.put("219", "DK"); // Denmark
|
||||
MID_TO_COUNTRY.put("220", "DK"); // Denmark
|
||||
MID_TO_COUNTRY.put("224", "ES"); // Spain
|
||||
MID_TO_COUNTRY.put("225", "ES"); // Spain
|
||||
MID_TO_COUNTRY.put("226", "FR"); // France
|
||||
MID_TO_COUNTRY.put("227", "FR"); // France
|
||||
MID_TO_COUNTRY.put("228", "FR"); // France
|
||||
MID_TO_COUNTRY.put("229", "MT"); // Malta
|
||||
MID_TO_COUNTRY.put("230", "FI"); // Finland
|
||||
MID_TO_COUNTRY.put("231", "FO"); // Faroe Islands
|
||||
MID_TO_COUNTRY.put("232", "GB"); // United Kingdom
|
||||
MID_TO_COUNTRY.put("233", "GB"); // United Kingdom
|
||||
MID_TO_COUNTRY.put("234", "GB"); // United Kingdom
|
||||
MID_TO_COUNTRY.put("235", "GB"); // United Kingdom
|
||||
MID_TO_COUNTRY.put("236", "GI"); // Gibraltar
|
||||
MID_TO_COUNTRY.put("237", "GR"); // Greece
|
||||
MID_TO_COUNTRY.put("238", "HR"); // Croatia
|
||||
MID_TO_COUNTRY.put("239", "GR"); // Greece
|
||||
MID_TO_COUNTRY.put("240", "GR"); // Greece
|
||||
MID_TO_COUNTRY.put("241", "GR"); // Greece
|
||||
MID_TO_COUNTRY.put("242", "MA"); // Morocco
|
||||
MID_TO_COUNTRY.put("243", "HU"); // Hungary
|
||||
MID_TO_COUNTRY.put("244", "NL"); // Netherlands
|
||||
MID_TO_COUNTRY.put("245", "NL"); // Netherlands
|
||||
MID_TO_COUNTRY.put("246", "NL"); // Netherlands
|
||||
MID_TO_COUNTRY.put("247", "IT"); // Italy
|
||||
MID_TO_COUNTRY.put("248", "MT"); // Malta
|
||||
MID_TO_COUNTRY.put("249", "MT"); // Malta
|
||||
MID_TO_COUNTRY.put("250", "IE"); // Ireland
|
||||
MID_TO_COUNTRY.put("251", "IS"); // Iceland
|
||||
MID_TO_COUNTRY.put("252", "LI"); // Liechtenstein
|
||||
MID_TO_COUNTRY.put("253", "LU"); // Luxembourg
|
||||
MID_TO_COUNTRY.put("254", "MC"); // Monaco
|
||||
MID_TO_COUNTRY.put("255", "PT"); // Portugal (Madeira)
|
||||
MID_TO_COUNTRY.put("256", "MT"); // Malta
|
||||
MID_TO_COUNTRY.put("257", "NO"); // Norway
|
||||
MID_TO_COUNTRY.put("258", "NO"); // Norway
|
||||
MID_TO_COUNTRY.put("259", "NO"); // Norway
|
||||
MID_TO_COUNTRY.put("261", "PL"); // Poland
|
||||
MID_TO_COUNTRY.put("262", "ME"); // Montenegro
|
||||
MID_TO_COUNTRY.put("263", "PT"); // Portugal
|
||||
MID_TO_COUNTRY.put("264", "RO"); // Romania
|
||||
MID_TO_COUNTRY.put("265", "SE"); // Sweden
|
||||
MID_TO_COUNTRY.put("266", "SE"); // Sweden
|
||||
MID_TO_COUNTRY.put("267", "SK"); // Slovakia
|
||||
MID_TO_COUNTRY.put("268", "SM"); // San Marino
|
||||
MID_TO_COUNTRY.put("269", "CH"); // Switzerland
|
||||
MID_TO_COUNTRY.put("270", "CZ"); // Czech Republic
|
||||
MID_TO_COUNTRY.put("271", "TR"); // Turkey
|
||||
MID_TO_COUNTRY.put("272", "UA"); // Ukraine
|
||||
MID_TO_COUNTRY.put("273", "RU"); // Russian Federation
|
||||
MID_TO_COUNTRY.put("274", "MK"); // North Macedonia
|
||||
MID_TO_COUNTRY.put("275", "LV"); // Latvia
|
||||
MID_TO_COUNTRY.put("276", "EE"); // Estonia
|
||||
MID_TO_COUNTRY.put("277", "LT"); // Lithuania
|
||||
MID_TO_COUNTRY.put("278", "SI"); // Slovenia
|
||||
MID_TO_COUNTRY.put("279", "RS"); // Serbia
|
||||
|
||||
// North America & Caribbean
|
||||
MID_TO_COUNTRY.put("301", "AI"); // Anguilla
|
||||
MID_TO_COUNTRY.put("303", "US"); // USA (Alaska)
|
||||
MID_TO_COUNTRY.put("304", "AG"); // Antigua and Barbuda
|
||||
MID_TO_COUNTRY.put("305", "AG"); // Antigua and Barbuda
|
||||
MID_TO_COUNTRY.put("306", "CW"); // Curaçao
|
||||
MID_TO_COUNTRY.put("307", "AW"); // Aruba
|
||||
MID_TO_COUNTRY.put("308", "BS"); // Bahamas
|
||||
MID_TO_COUNTRY.put("309", "BS"); // Bahamas
|
||||
MID_TO_COUNTRY.put("310", "BM"); // Bermuda
|
||||
MID_TO_COUNTRY.put("311", "BS"); // Bahamas
|
||||
MID_TO_COUNTRY.put("312", "BZ"); // Belize
|
||||
MID_TO_COUNTRY.put("314", "BB"); // Barbados
|
||||
MID_TO_COUNTRY.put("316", "CA"); // Canada
|
||||
MID_TO_COUNTRY.put("319", "KY"); // Cayman Islands
|
||||
MID_TO_COUNTRY.put("321", "CR"); // Costa Rica
|
||||
MID_TO_COUNTRY.put("323", "CU"); // Cuba
|
||||
MID_TO_COUNTRY.put("325", "DM"); // Dominica
|
||||
MID_TO_COUNTRY.put("327", "DO"); // Dominican Republic
|
||||
MID_TO_COUNTRY.put("329", "GP"); // Guadeloupe
|
||||
MID_TO_COUNTRY.put("330", "GD"); // Grenada
|
||||
MID_TO_COUNTRY.put("331", "GL"); // Greenland
|
||||
MID_TO_COUNTRY.put("332", "GT"); // Guatemala
|
||||
MID_TO_COUNTRY.put("334", "HN"); // Honduras
|
||||
MID_TO_COUNTRY.put("336", "HT"); // Haiti
|
||||
MID_TO_COUNTRY.put("338", "US"); // USA
|
||||
MID_TO_COUNTRY.put("339", "JM"); // Jamaica
|
||||
MID_TO_COUNTRY.put("341", "KN"); // Saint Kitts and Nevis
|
||||
MID_TO_COUNTRY.put("343", "LC"); // Saint Lucia
|
||||
MID_TO_COUNTRY.put("345", "MX"); // Mexico
|
||||
MID_TO_COUNTRY.put("347", "MQ"); // Martinique
|
||||
MID_TO_COUNTRY.put("348", "MS"); // Montserrat
|
||||
MID_TO_COUNTRY.put("350", "NI"); // Nicaragua
|
||||
MID_TO_COUNTRY.put("351", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("352", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("353", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("354", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("355", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("356", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("357", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("358", "PR"); // Puerto Rico
|
||||
MID_TO_COUNTRY.put("359", "SV"); // El Salvador
|
||||
MID_TO_COUNTRY.put("361", "PM"); // Saint Pierre and Miquelon
|
||||
MID_TO_COUNTRY.put("362", "TT"); // Trinidad and Tobago
|
||||
MID_TO_COUNTRY.put("364", "TC"); // Turks and Caicos Islands
|
||||
MID_TO_COUNTRY.put("366", "US"); // USA
|
||||
MID_TO_COUNTRY.put("367", "US"); // USA
|
||||
MID_TO_COUNTRY.put("368", "US"); // USA
|
||||
MID_TO_COUNTRY.put("369", "US"); // USA
|
||||
MID_TO_COUNTRY.put("370", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("371", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("372", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("373", "PA"); // Panama
|
||||
MID_TO_COUNTRY.put("375", "VC"); // Saint Vincent and the Grenadines
|
||||
MID_TO_COUNTRY.put("376", "VC"); // Saint Vincent and the Grenadines
|
||||
MID_TO_COUNTRY.put("377", "VC"); // Saint Vincent and the Grenadines
|
||||
MID_TO_COUNTRY.put("378", "VG"); // British Virgin Islands
|
||||
MID_TO_COUNTRY.put("379", "VI"); // U.S. Virgin Islands
|
||||
|
||||
// Asia & Middle East
|
||||
MID_TO_COUNTRY.put("401", "AF"); // Afghanistan
|
||||
MID_TO_COUNTRY.put("403", "SA"); // Saudi Arabia
|
||||
MID_TO_COUNTRY.put("405", "BD"); // Bangladesh
|
||||
MID_TO_COUNTRY.put("408", "BH"); // Bahrain
|
||||
MID_TO_COUNTRY.put("410", "BT"); // Bhutan
|
||||
MID_TO_COUNTRY.put("412", "CN"); // China
|
||||
MID_TO_COUNTRY.put("413", "CN"); // China
|
||||
MID_TO_COUNTRY.put("414", "CN"); // China
|
||||
MID_TO_COUNTRY.put("416", "TW"); // Taiwan
|
||||
MID_TO_COUNTRY.put("417", "LK"); // Sri Lanka
|
||||
MID_TO_COUNTRY.put("419", "IN"); // India
|
||||
MID_TO_COUNTRY.put("422", "IR"); // Iran
|
||||
MID_TO_COUNTRY.put("423", "AZ"); // Azerbaijan
|
||||
MID_TO_COUNTRY.put("425", "IQ"); // Iraq
|
||||
MID_TO_COUNTRY.put("428", "IL"); // Israel
|
||||
MID_TO_COUNTRY.put("431", "JP"); // Japan
|
||||
MID_TO_COUNTRY.put("432", "JP"); // Japan
|
||||
MID_TO_COUNTRY.put("434", "TM"); // Turkmenistan
|
||||
MID_TO_COUNTRY.put("436", "KZ"); // Kazakhstan
|
||||
MID_TO_COUNTRY.put("437", "UZ"); // Uzbekistan
|
||||
MID_TO_COUNTRY.put("438", "JO"); // Jordan
|
||||
MID_TO_COUNTRY.put("440", "KR"); // South Korea
|
||||
MID_TO_COUNTRY.put("441", "KR"); // South Korea
|
||||
MID_TO_COUNTRY.put("443", "PS"); // Palestine
|
||||
MID_TO_COUNTRY.put("445", "KP"); // North Korea
|
||||
MID_TO_COUNTRY.put("447", "KW"); // Kuwait
|
||||
MID_TO_COUNTRY.put("450", "LB"); // Lebanon
|
||||
MID_TO_COUNTRY.put("451", "KG"); // Kyrgyzstan
|
||||
MID_TO_COUNTRY.put("453", "MO"); // Macao
|
||||
MID_TO_COUNTRY.put("455", "MV"); // Maldives
|
||||
MID_TO_COUNTRY.put("457", "MN"); // Mongolia
|
||||
MID_TO_COUNTRY.put("459", "NP"); // Nepal
|
||||
MID_TO_COUNTRY.put("461", "OM"); // Oman
|
||||
MID_TO_COUNTRY.put("463", "PK"); // Pakistan
|
||||
MID_TO_COUNTRY.put("466", "QA"); // Qatar
|
||||
MID_TO_COUNTRY.put("468", "SY"); // Syria
|
||||
MID_TO_COUNTRY.put("470", "AE"); // United Arab Emirates
|
||||
MID_TO_COUNTRY.put("471", "AE"); // United Arab Emirates
|
||||
MID_TO_COUNTRY.put("472", "TJ"); // Tajikistan
|
||||
MID_TO_COUNTRY.put("473", "YE"); // Yemen
|
||||
MID_TO_COUNTRY.put("475", "YE"); // Yemen
|
||||
MID_TO_COUNTRY.put("477", "HK"); // Hong Kong
|
||||
MID_TO_COUNTRY.put("478", "BA"); // Bosnia and Herzegovina (legacy routing usage)
|
||||
|
||||
// Oceania
|
||||
MID_TO_COUNTRY.put("501", "AQ"); // Antarctica
|
||||
MID_TO_COUNTRY.put("503", "AU"); // Australia
|
||||
MID_TO_COUNTRY.put("506", "MM"); // Myanmar
|
||||
MID_TO_COUNTRY.put("508", "BN"); // Brunei
|
||||
MID_TO_COUNTRY.put("510", "FM"); // Micronesia
|
||||
MID_TO_COUNTRY.put("511", "PW"); // Palau
|
||||
MID_TO_COUNTRY.put("512", "NZ"); // New Zealand
|
||||
MID_TO_COUNTRY.put("514", "KH"); // Cambodia
|
||||
MID_TO_COUNTRY.put("515", "KH"); // Cambodia
|
||||
MID_TO_COUNTRY.put("516", "CX"); // Christmas Island
|
||||
MID_TO_COUNTRY.put("518", "CK"); // Cook Islands
|
||||
MID_TO_COUNTRY.put("520", "FJ"); // Fiji
|
||||
MID_TO_COUNTRY.put("523", "CC"); // Cocos (Keeling) Islands
|
||||
MID_TO_COUNTRY.put("525", "ID"); // Indonesia
|
||||
MID_TO_COUNTRY.put("529", "KI"); // Kiribati
|
||||
MID_TO_COUNTRY.put("531", "LA"); // Laos
|
||||
MID_TO_COUNTRY.put("533", "MY"); // Malaysia
|
||||
MID_TO_COUNTRY.put("536", "MP"); // Northern Mariana Islands
|
||||
MID_TO_COUNTRY.put("538", "MH"); // Marshall Islands
|
||||
MID_TO_COUNTRY.put("540", "NC"); // New Caledonia
|
||||
MID_TO_COUNTRY.put("542", "NU"); // Niue
|
||||
MID_TO_COUNTRY.put("544", "NR"); // Nauru
|
||||
MID_TO_COUNTRY.put("546", "PF"); // French Polynesia
|
||||
MID_TO_COUNTRY.put("548", "PH"); // Philippines
|
||||
MID_TO_COUNTRY.put("553", "PG"); // Papua New Guinea
|
||||
MID_TO_COUNTRY.put("555", "PN"); // Pitcairn Islands
|
||||
MID_TO_COUNTRY.put("557", "SB"); // Solomon Islands
|
||||
MID_TO_COUNTRY.put("559", "AS"); // American Samoa
|
||||
MID_TO_COUNTRY.put("561", "WS"); // Samoa
|
||||
MID_TO_COUNTRY.put("563", "SG"); // Singapore
|
||||
MID_TO_COUNTRY.put("564", "SG"); // Singapore
|
||||
MID_TO_COUNTRY.put("565", "SG"); // Singapore
|
||||
MID_TO_COUNTRY.put("566", "SG"); // Singapore
|
||||
MID_TO_COUNTRY.put("567", "TH"); // Thailand
|
||||
MID_TO_COUNTRY.put("570", "TO"); // Tonga
|
||||
MID_TO_COUNTRY.put("572", "TV"); // Tuvalu
|
||||
MID_TO_COUNTRY.put("574", "VN"); // Vietnam
|
||||
MID_TO_COUNTRY.put("576", "VU"); // Vanuatu
|
||||
MID_TO_COUNTRY.put("578", "WF"); // Wallis and Futuna
|
||||
|
||||
// Africa
|
||||
MID_TO_COUNTRY.put("601", "ZA"); // South Africa
|
||||
MID_TO_COUNTRY.put("603", "AO"); // Angola
|
||||
MID_TO_COUNTRY.put("605", "DZ"); // Algeria
|
||||
MID_TO_COUNTRY.put("609", "BI"); // Burundi
|
||||
MID_TO_COUNTRY.put("610", "BJ"); // Benin
|
||||
MID_TO_COUNTRY.put("611", "BW"); // Botswana
|
||||
MID_TO_COUNTRY.put("612", "CF"); // Central African Republic
|
||||
MID_TO_COUNTRY.put("613", "CM"); // Cameroon
|
||||
MID_TO_COUNTRY.put("615", "CG"); // Congo (Republic)
|
||||
MID_TO_COUNTRY.put("616", "KM"); // Comoros
|
||||
MID_TO_COUNTRY.put("617", "CV"); // Cabo Verde
|
||||
MID_TO_COUNTRY.put("619", "CI"); // Côte d’Ivoire
|
||||
MID_TO_COUNTRY.put("621", "DJ"); // Djibouti
|
||||
MID_TO_COUNTRY.put("622", "EG"); // Egypt
|
||||
MID_TO_COUNTRY.put("624", "ET"); // Ethiopia
|
||||
MID_TO_COUNTRY.put("625", "ER"); // Eritrea
|
||||
MID_TO_COUNTRY.put("626", "GA"); // Gabon
|
||||
MID_TO_COUNTRY.put("627", "GH"); // Ghana
|
||||
MID_TO_COUNTRY.put("629", "GM"); // Gambia
|
||||
MID_TO_COUNTRY.put("630", "GW"); // Guinea-Bissau
|
||||
MID_TO_COUNTRY.put("631", "GQ"); // Equatorial Guinea
|
||||
MID_TO_COUNTRY.put("632", "GN"); // Guinea
|
||||
MID_TO_COUNTRY.put("633", "BF"); // Burkina Faso
|
||||
MID_TO_COUNTRY.put("634", "KE"); // Kenya
|
||||
MID_TO_COUNTRY.put("636", "LR"); // Liberia
|
||||
MID_TO_COUNTRY.put("637", "LR"); // Liberia
|
||||
MID_TO_COUNTRY.put("642", "LY"); // Libya
|
||||
MID_TO_COUNTRY.put("644", "LS"); // Lesotho
|
||||
MID_TO_COUNTRY.put("645", "MU"); // Mauritius
|
||||
MID_TO_COUNTRY.put("647", "MG"); // Madagascar
|
||||
MID_TO_COUNTRY.put("649", "ML"); // Mali
|
||||
MID_TO_COUNTRY.put("650", "MZ"); // Mozambique
|
||||
MID_TO_COUNTRY.put("654", "MR"); // Mauritania
|
||||
MID_TO_COUNTRY.put("655", "MW"); // Malawi
|
||||
MID_TO_COUNTRY.put("656", "NE"); // Niger
|
||||
MID_TO_COUNTRY.put("657", "NG"); // Nigeria
|
||||
MID_TO_COUNTRY.put("659", "NA"); // Namibia
|
||||
MID_TO_COUNTRY.put("660", "RE"); // Reunion (FR)
|
||||
MID_TO_COUNTRY.put("661", "RW"); // Rwanda
|
||||
MID_TO_COUNTRY.put("662", "SD"); // Sudan
|
||||
MID_TO_COUNTRY.put("663", "SN"); // Senegal
|
||||
MID_TO_COUNTRY.put("664", "SC"); // Seychelles
|
||||
MID_TO_COUNTRY.put("665", "SH"); // Saint Helena
|
||||
MID_TO_COUNTRY.put("666", "SO"); // Somalia
|
||||
MID_TO_COUNTRY.put("667", "SL"); // Sierra Leone
|
||||
MID_TO_COUNTRY.put("668", "ST"); // Sao Tome and Principe
|
||||
MID_TO_COUNTRY.put("669", "SZ"); // Eswatini
|
||||
MID_TO_COUNTRY.put("670", "TD"); // Chad
|
||||
MID_TO_COUNTRY.put("671", "TG"); // Togo
|
||||
MID_TO_COUNTRY.put("672", "TN"); // Tunisia
|
||||
MID_TO_COUNTRY.put("674", "TZ"); // Tanzania
|
||||
MID_TO_COUNTRY.put("675", "UG"); // Uganda
|
||||
MID_TO_COUNTRY.put("676", "CD"); // DR Congo
|
||||
MID_TO_COUNTRY.put("677", "TZ"); // Tanzania (alt)
|
||||
MID_TO_COUNTRY.put("678", "ZM"); // Zambia
|
||||
MID_TO_COUNTRY.put("679", "ZW"); // Zimbabwe
|
||||
|
||||
// South America
|
||||
MID_TO_COUNTRY.put("701", "AR"); // Argentina
|
||||
MID_TO_COUNTRY.put("710", "BR"); // Brazil
|
||||
MID_TO_COUNTRY.put("720", "BO"); // Bolivia
|
||||
MID_TO_COUNTRY.put("725", "CL"); // Chile
|
||||
MID_TO_COUNTRY.put("730", "CO"); // Colombia
|
||||
MID_TO_COUNTRY.put("735", "EC"); // Ecuador
|
||||
MID_TO_COUNTRY.put("740", "FK"); // Falkland Islands
|
||||
MID_TO_COUNTRY.put("745", "GF"); // French Guiana
|
||||
MID_TO_COUNTRY.put("750", "GY"); // Guyana
|
||||
MID_TO_COUNTRY.put("755", "PY"); // Paraguay
|
||||
MID_TO_COUNTRY.put("760", "PE"); // Peru
|
||||
MID_TO_COUNTRY.put("765", "SR"); // Suriname
|
||||
MID_TO_COUNTRY.put("770", "UY"); // Uruguay
|
||||
MID_TO_COUNTRY.put("775", "VE"); // Venezuela
|
||||
}
|
||||
|
||||
private MIDToCountry() {}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.grigowashere.aismap.utils;
|
||||
|
||||
/**
|
||||
* Утилиты для навигационных вычислений
|
||||
*/
|
||||
public class NavigationUtils {
|
||||
|
||||
// Радиус Земли в метрах
|
||||
private static final double EARTH_RADIUS_METERS = 6371000.0;
|
||||
|
||||
/**
|
||||
* Вычисляет расстояние между двумя точками на Земле (формула гаверсинуса)
|
||||
* @param lat1 широта первой точки в градусах
|
||||
* @param lon1 долгота первой точки в градусах
|
||||
* @param lat2 широта второй точки в градусах
|
||||
* @param lon2 долгота второй точки в градусах
|
||||
* @return расстояние в метрах
|
||||
*/
|
||||
public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
// Проверяем валидность координат
|
||||
if (lat1 == 0 && lon1 == 0) return -1;
|
||||
if (lat2 == 0 && lon2 == 0) return -1;
|
||||
|
||||
// Преобразуем градусы в радианы
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lon1Rad = Math.toRadians(lon1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double lon2Rad = Math.toRadians(lon2);
|
||||
|
||||
// Разности координат
|
||||
double deltaLat = lat2Rad - lat1Rad;
|
||||
double deltaLon = lon2Rad - lon1Rad;
|
||||
|
||||
// Формула гаверсинуса
|
||||
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_METERS * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет азимут (bearing) от первой точки ко второй
|
||||
* @param lat1 широта первой точки в градусах
|
||||
* @param lon1 долгота первой точки в градусах
|
||||
* @param lat2 широта второй точки в градусах
|
||||
* @param lon2 долгота второй точки в градусах
|
||||
* @return азимут в градусах (0-360)
|
||||
*/
|
||||
public static double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
|
||||
// Проверяем валидность координат
|
||||
if (lat1 == 0 && lon1 == 0) return -1;
|
||||
if (lat2 == 0 && lon2 == 0) return -1;
|
||||
|
||||
// Преобразуем градусы в радианы
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lon1Rad = Math.toRadians(lon1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double lon2Rad = Math.toRadians(lon2);
|
||||
|
||||
// Разности координат
|
||||
double deltaLon = lon2Rad - lon1Rad;
|
||||
|
||||
// Вычисляем азимут
|
||||
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 bearingRad = Math.atan2(y, x);
|
||||
double bearingDeg = Math.toDegrees(bearingRad);
|
||||
|
||||
// Нормализуем к диапазону 0-360
|
||||
return (bearingDeg + 360) % 360;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет относительный азимут (сколько градусов влево/вправо от нашего курса)
|
||||
* @param ourCourse наш курс в градусах (0-360)
|
||||
* @param targetBearing азимут до цели в градусах (0-360)
|
||||
* @return относительный азимут в градусах (-180 до +180, отрицательное = влево, положительное = вправо)
|
||||
*/
|
||||
public static double calculateRelativeBearing(double ourCourse, double targetBearing) {
|
||||
if (ourCourse < 0 || targetBearing < 0) return -1;
|
||||
|
||||
double relativeBearing = targetBearing - ourCourse;
|
||||
|
||||
// Нормализуем к диапазону -180 до +180
|
||||
while (relativeBearing > 180) relativeBearing -= 360;
|
||||
while (relativeBearing < -180) relativeBearing += 360;
|
||||
|
||||
return relativeBearing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует расстояние для отображения
|
||||
* @param distanceMeters расстояние в метрах
|
||||
* @return отформатированная строка
|
||||
*/
|
||||
public static String formatDistance(double distanceMeters) {
|
||||
if (distanceMeters < 0) return "--";
|
||||
|
||||
if (distanceMeters < 1000) {
|
||||
return String.format("%.0f м", distanceMeters);
|
||||
} else {
|
||||
return String.format("%.1f км", distanceMeters / 1000.0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует относительный азимут для отображения
|
||||
* @param relativeBearing относительный азимут в градусах
|
||||
* @return отформатированная строка
|
||||
*/
|
||||
public static String formatRelativeBearing(double relativeBearing) {
|
||||
// Проверяем на невалидные значения
|
||||
if (relativeBearing < -180 || relativeBearing > 180) return "--";
|
||||
|
||||
if (Math.abs(relativeBearing) < 1) {
|
||||
return "прямо";
|
||||
} else if (relativeBearing > 0) {
|
||||
return String.format("%.0f° вправо", relativeBearing);
|
||||
} else {
|
||||
return String.format("%.0f° влево", Math.abs(relativeBearing));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,19 @@ public class SettingsManager {
|
||||
private static final String KEY_ANDROID_NMEA_ENABLED = "android_nmea_enabled";
|
||||
private static final String KEY_UDP_NMEA_ENABLED = "udp_nmea_enabled";
|
||||
private static final String KEY_DATA_MODE = "data_mode";
|
||||
private static final String KEY_DATA_STALE_WARNING_MINUTES = "data_stale_warning_minutes";
|
||||
private static final String KEY_DATA_STALE_REMOVE_MINUTES = "data_stale_remove_minutes";
|
||||
private static final String KEY_PATH_TRACKING_ENABLED = "path_tracking_enabled";
|
||||
private static final String KEY_PATH_COLOR = "path_color";
|
||||
private static final String KEY_PREDICTION_COLOR = "prediction_color";
|
||||
private static final String KEY_PATH_WIDTH = "path_width";
|
||||
private static final String KEY_PREDICTION_WIDTH = "prediction_width";
|
||||
private static final String KEY_PATH_MAX_POINTS = "path_max_points";
|
||||
private static final String KEY_PREDICTION_HORIZON_SEC = "prediction_horizon_sec";
|
||||
private static final String KEY_VIBRATION_ENABLED = "vibration_enabled";
|
||||
private static final String KEY_SOUND_ENABLED = "sound_enabled";
|
||||
private static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled";
|
||||
private static final String KEY_CURSOR_ENABLED = "cursor_enabled";
|
||||
|
||||
// Значения по умолчанию
|
||||
private static final int DEFAULT_UDP_PORT = 10110;
|
||||
@@ -26,6 +39,19 @@ public class SettingsManager {
|
||||
private static final boolean DEFAULT_ANDROID_NMEA_ENABLED = true;
|
||||
private static final boolean DEFAULT_UDP_NMEA_ENABLED = true;
|
||||
private static final String DEFAULT_DATA_MODE = "hybrid";
|
||||
private static final int DEFAULT_DATA_STALE_WARNING_MINUTES = 5; // Показывать losingtarget.xml
|
||||
private static final int DEFAULT_DATA_STALE_REMOVE_MINUTES = 7; // Удалять из списка
|
||||
private static final boolean DEFAULT_PATH_TRACKING_ENABLED = true;
|
||||
private static final int DEFAULT_PATH_COLOR = 0xFF00FFFF; // Голубой
|
||||
private static final int DEFAULT_PREDICTION_COLOR = 0xFFFFFF00; // Желтый
|
||||
private static final float DEFAULT_PATH_WIDTH = 3.0f;
|
||||
private static final float DEFAULT_PREDICTION_WIDTH = 2.0f;
|
||||
private static final int DEFAULT_PATH_MAX_POINTS = 100; // Уменьшено с 300 до 100 для предотвращения зависаний
|
||||
private static final int DEFAULT_PREDICTION_HORIZON_SEC = 60;
|
||||
private static final boolean DEFAULT_VIBRATION_ENABLED = true;
|
||||
private static final boolean DEFAULT_SOUND_ENABLED = true;
|
||||
private static final boolean DEFAULT_KEEP_SCREEN_ON_ENABLED = true;
|
||||
private static final boolean DEFAULT_CURSOR_ENABLED = false;
|
||||
|
||||
// Режимы работы с данными
|
||||
public static final String DATA_MODE_HYBRID = "hybrid";
|
||||
@@ -163,10 +189,35 @@ public class SettingsManager {
|
||||
.putBoolean(KEY_ANDROID_NMEA_ENABLED, DEFAULT_ANDROID_NMEA_ENABLED)
|
||||
.putBoolean(KEY_UDP_NMEA_ENABLED, DEFAULT_UDP_NMEA_ENABLED)
|
||||
.putString(KEY_DATA_MODE, DEFAULT_DATA_MODE)
|
||||
.putInt(KEY_DATA_STALE_WARNING_MINUTES, DEFAULT_DATA_STALE_WARNING_MINUTES)
|
||||
.putInt(KEY_DATA_STALE_REMOVE_MINUTES, DEFAULT_DATA_STALE_REMOVE_MINUTES)
|
||||
.putBoolean(KEY_VIBRATION_ENABLED, DEFAULT_VIBRATION_ENABLED)
|
||||
.putBoolean(KEY_SOUND_ENABLED, DEFAULT_SOUND_ENABLED)
|
||||
.putBoolean(KEY_KEEP_SCREEN_ON_ENABLED, DEFAULT_KEEP_SCREEN_ON_ENABLED)
|
||||
.apply();
|
||||
Log.i(TAG, "Настройки сброшены к значениям по умолчанию");
|
||||
}
|
||||
|
||||
public int getPathMaxPoints() {
|
||||
return prefs.getInt(KEY_PATH_MAX_POINTS, DEFAULT_PATH_MAX_POINTS);
|
||||
}
|
||||
|
||||
public void setPathMaxPoints(int maxPoints) {
|
||||
if (maxPoints < 10) maxPoints = 10;
|
||||
if (maxPoints > 10000) maxPoints = 10000;
|
||||
prefs.edit().putInt(KEY_PATH_MAX_POINTS, maxPoints).apply();
|
||||
}
|
||||
|
||||
public int getPredictionHorizonSec() {
|
||||
return prefs.getInt(KEY_PREDICTION_HORIZON_SEC, DEFAULT_PREDICTION_HORIZON_SEC);
|
||||
}
|
||||
|
||||
public void setPredictionHorizonSec(int seconds) {
|
||||
if (seconds < 5) seconds = 5;
|
||||
if (seconds > 3600) seconds = 3600;
|
||||
prefs.edit().putInt(KEY_PREDICTION_HORIZON_SEC, seconds).apply();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает все настройки в виде строки для отладки
|
||||
*/
|
||||
@@ -175,12 +226,15 @@ public class SettingsManager {
|
||||
"UDP: порт=%d, включен=%s\n" +
|
||||
"Android NMEA: %s\n" +
|
||||
"UDP NMEA: %s\n" +
|
||||
"Режим данных: %s",
|
||||
"Режим данных: %s\n" +
|
||||
"Уведомления: вибрация=%s, звук=%s",
|
||||
getUDPPort(),
|
||||
isUDPEnabled() ? "да" : "нет",
|
||||
isAndroidNMEAEnabled() ? "включен" : "выключен",
|
||||
isUDPNMEAEnabled() ? "включен" : "выключен",
|
||||
getDataMode()
|
||||
getDataMode(),
|
||||
isVibrationEnabled() ? "включена" : "выключена",
|
||||
isSoundEnabled() ? "включен" : "выключен"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -199,4 +253,186 @@ public class SettingsManager {
|
||||
isUDPNMEAEnabled() != currentUDPNMEA ||
|
||||
!getDataMode().equals(currentDataMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает время предупреждения об устаревших данных (в минутах)
|
||||
*/
|
||||
public int getDataStaleWarningMinutes() {
|
||||
return prefs.getInt(KEY_DATA_STALE_WARNING_MINUTES, DEFAULT_DATA_STALE_WARNING_MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает время предупреждения об устаревших данных (в минутах)
|
||||
*/
|
||||
public void setDataStaleWarningMinutes(int minutes) {
|
||||
if (minutes < 1 || minutes > 60) {
|
||||
Log.w(TAG, "Некорректное время предупреждения: " + minutes + ", используем значение по умолчанию");
|
||||
minutes = DEFAULT_DATA_STALE_WARNING_MINUTES;
|
||||
}
|
||||
prefs.edit().putInt(KEY_DATA_STALE_WARNING_MINUTES, minutes).apply();
|
||||
Log.i(TAG, "Время предупреждения об устаревших данных установлено: " + minutes + " минут");
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает время удаления устаревших данных (в минутах)
|
||||
*/
|
||||
public int getDataStaleRemoveMinutes() {
|
||||
return prefs.getInt(KEY_DATA_STALE_REMOVE_MINUTES, DEFAULT_DATA_STALE_REMOVE_MINUTES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает время удаления устаревших данных (в минутах)
|
||||
*/
|
||||
public void setDataStaleRemoveMinutes(int minutes) {
|
||||
if (minutes < 1 || minutes > 60) {
|
||||
Log.w(TAG, "Некорректное время удаления: " + minutes + ", используем значение по умолчанию");
|
||||
minutes = DEFAULT_DATA_STALE_REMOVE_MINUTES;
|
||||
}
|
||||
prefs.edit().putInt(KEY_DATA_STALE_REMOVE_MINUTES, minutes).apply();
|
||||
Log.i(TAG, "Время удаления устаревших данных установлено: " + minutes + " минут");
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включено ли отслеживание путей
|
||||
*/
|
||||
public boolean isPathTrackingEnabled() {
|
||||
return prefs.getBoolean(KEY_PATH_TRACKING_ENABLED, DEFAULT_PATH_TRACKING_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает отслеживание путей
|
||||
*/
|
||||
public void setPathTrackingEnabled(boolean enabled) {
|
||||
prefs.edit().putBoolean(KEY_PATH_TRACKING_ENABLED, enabled).apply();
|
||||
Log.i(TAG, "Отслеживание путей: " + (enabled ? "включено" : "выключено"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает цвет пройденного пути
|
||||
*/
|
||||
public int getPathColor() {
|
||||
return prefs.getInt(KEY_PATH_COLOR, DEFAULT_PATH_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает цвет пройденного пути
|
||||
*/
|
||||
public void setPathColor(int color) {
|
||||
prefs.edit().putInt(KEY_PATH_COLOR, color).apply();
|
||||
Log.i(TAG, "Цвет пройденного пути установлен: " + String.format("#%08X", color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает цвет прогнозируемого пути
|
||||
*/
|
||||
public int getPredictionColor() {
|
||||
return prefs.getInt(KEY_PREDICTION_COLOR, DEFAULT_PREDICTION_COLOR);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает цвет прогнозируемого пути
|
||||
*/
|
||||
public void setPredictionColor(int color) {
|
||||
prefs.edit().putInt(KEY_PREDICTION_COLOR, color).apply();
|
||||
Log.i(TAG, "Цвет прогнозируемого пути установлен: " + String.format("#%08X", color));
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает ширину линии пройденного пути
|
||||
*/
|
||||
public float getPathWidth() {
|
||||
return prefs.getFloat(KEY_PATH_WIDTH, DEFAULT_PATH_WIDTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает ширину линии пройденного пути
|
||||
*/
|
||||
public void setPathWidth(float width) {
|
||||
if (width < 1.0f || width > 10.0f) {
|
||||
Log.w(TAG, "Некорректная ширина пути: " + width + ", используем значение по умолчанию");
|
||||
width = DEFAULT_PATH_WIDTH;
|
||||
}
|
||||
prefs.edit().putFloat(KEY_PATH_WIDTH, width).apply();
|
||||
Log.i(TAG, "Ширина пройденного пути установлена: " + width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Получает ширину линии прогнозируемого пути
|
||||
*/
|
||||
public float getPredictionWidth() {
|
||||
return prefs.getFloat(KEY_PREDICTION_WIDTH, DEFAULT_PREDICTION_WIDTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает ширину линии прогнозируемого пути
|
||||
*/
|
||||
public void setPredictionWidth(float width) {
|
||||
if (width < 1.0f || width > 10.0f) {
|
||||
Log.w(TAG, "Некорректная ширина прогноза: " + width + ", используем значение по умолчанию");
|
||||
width = DEFAULT_PREDICTION_WIDTH;
|
||||
}
|
||||
prefs.edit().putFloat(KEY_PREDICTION_WIDTH, width).apply();
|
||||
Log.i(TAG, "Ширина прогнозируемого пути установлена: " + width);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включена ли вибрация при обнаружении новых AIS целей
|
||||
*/
|
||||
public boolean isVibrationEnabled() {
|
||||
return prefs.getBoolean(KEY_VIBRATION_ENABLED, DEFAULT_VIBRATION_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает вибрацию при обнаружении новых AIS целей
|
||||
*/
|
||||
public void setVibrationEnabled(boolean enabled) {
|
||||
prefs.edit().putBoolean(KEY_VIBRATION_ENABLED, enabled).apply();
|
||||
Log.i(TAG, "Вибрация при обнаружении новых AIS целей: " + (enabled ? "включена" : "выключена"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли звук при обнаружении новых AIS целей
|
||||
*/
|
||||
public boolean isSoundEnabled() {
|
||||
return prefs.getBoolean(KEY_SOUND_ENABLED, DEFAULT_SOUND_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает звук при обнаружении новых AIS целей
|
||||
*/
|
||||
public void setSoundEnabled(boolean enabled) {
|
||||
prefs.edit().putBoolean(KEY_SOUND_ENABLED, enabled).apply();
|
||||
Log.i(TAG, "Звук при обнаружении новых AIS целей: " + (enabled ? "включен" : "выключен"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли режим "не засыпать" для экрана
|
||||
*/
|
||||
public boolean isKeepScreenOnEnabled() {
|
||||
return prefs.getBoolean(KEY_KEEP_SCREEN_ON_ENABLED, DEFAULT_KEEP_SCREEN_ON_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает режим "не засыпать" для экрана
|
||||
*/
|
||||
public void setKeepScreenOnEnabled(boolean enabled) {
|
||||
prefs.edit().putBoolean(KEY_KEEP_SCREEN_ON_ENABLED, enabled).apply();
|
||||
Log.i(TAG, "Режим 'не засыпать' для экрана: " + (enabled ? "включен" : "выключен"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, включен ли курсор на карте
|
||||
*/
|
||||
public boolean isCursorEnabled() {
|
||||
return prefs.getBoolean(KEY_CURSOR_ENABLED, DEFAULT_CURSOR_ENABLED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Включает/выключает курсор на карте
|
||||
*/
|
||||
public void setCursorEnabled(boolean enabled) {
|
||||
prefs.edit().putBoolean(KEY_CURSOR_ENABLED, enabled).apply();
|
||||
Log.i(TAG, "Курсор на карте: " + (enabled ? "включен" : "выключен"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -69,12 +69,28 @@ public class CompassView extends BaseDockWidget {
|
||||
}
|
||||
|
||||
private float getShortestRotation(float start, float end) {
|
||||
// Нормализуем углы к диапазону 0-360
|
||||
start = normalizeAngle(start);
|
||||
end = normalizeAngle(end);
|
||||
|
||||
float diff = end - start;
|
||||
while (diff > 180) diff -= 360;
|
||||
while (diff < -180) diff += 360;
|
||||
|
||||
// Если разность больше 180°, идем в обратную сторону
|
||||
if (diff > 180) {
|
||||
diff -= 360;
|
||||
} else if (diff < -180) {
|
||||
diff += 360;
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
private float normalizeAngle(float angle) {
|
||||
while (angle < 0) angle += 360;
|
||||
while (angle >= 360) angle -= 360;
|
||||
return angle;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Прямая шкала (dock-режим)
|
||||
@@ -109,9 +125,11 @@ public class CompassView extends BaseDockWidget {
|
||||
// Плавное обновление азимута
|
||||
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;
|
||||
// Ограничиваем максимальное изменение за один кадр
|
||||
float maxChange = 3.0f; // максимальное изменение в градусах за кадр
|
||||
float change = Math.signum(diff) * Math.min(Math.abs(diff * SMOOTHING_FACTOR), maxChange);
|
||||
currentAzimuth += change;
|
||||
currentAzimuth = normalizeAngle(currentAzimuth);
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
@@ -123,8 +141,7 @@ public class CompassView extends BaseDockWidget {
|
||||
// Рисуем деления шкалы
|
||||
for (int degree = 0; degree < 360; degree += 15) {
|
||||
// Вычисляем относительное положение деления
|
||||
float relativeDegree = (degree - currentAzimuth + 360) % 360;
|
||||
if (relativeDegree > 180) relativeDegree -= 360;
|
||||
float relativeDegree = getShortestRotation(currentAzimuth, degree);
|
||||
|
||||
// Рисуем только видимые деления
|
||||
if (Math.abs(relativeDegree) <= visibleDegrees / 2) {
|
||||
@@ -149,8 +166,7 @@ public class CompassView extends BaseDockWidget {
|
||||
|
||||
// Рисуем суда
|
||||
for (AISVessel vessel : nearbyVessels) {
|
||||
float relativeBearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
|
||||
if (relativeBearing > 180) relativeBearing -= 360;
|
||||
float relativeBearing = getShortestRotation(currentAzimuth, (float) vessel.getCourse());
|
||||
if (Math.abs(relativeBearing) <= visibleDegrees / 2) {
|
||||
float x = centerX + (relativeBearing / (visibleDegrees / 2)) * (w / 2);
|
||||
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
|
||||
@@ -218,9 +234,11 @@ public class CompassView extends BaseDockWidget {
|
||||
// Плавное обновление азимута
|
||||
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;
|
||||
// Ограничиваем максимальное изменение за один кадр
|
||||
float maxChange = 3.0f; // максимальное изменение в градусах за кадр
|
||||
float change = Math.signum(diff) * Math.min(Math.abs(diff * SMOOTHING_FACTOR), maxChange);
|
||||
currentAzimuth += change;
|
||||
currentAzimuth = normalizeAngle(currentAzimuth);
|
||||
postInvalidateOnAnimation();
|
||||
}
|
||||
|
||||
@@ -246,7 +264,7 @@ public class CompassView extends BaseDockWidget {
|
||||
|
||||
// Рисуем суда по кругу
|
||||
for (AISVessel vessel : nearbyVessels) {
|
||||
float bearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
|
||||
float bearing = getShortestRotation(currentAzimuth, (float) vessel.getCourse());
|
||||
float angle = (float) Math.toRadians(bearing);
|
||||
float vesselRadius = radius * 0.6f;
|
||||
float vx = cx + (float) Math.sin(angle) * vesselRadius;
|
||||
@@ -313,7 +331,25 @@ public class CompassView extends BaseDockWidget {
|
||||
}
|
||||
|
||||
public void setAzimuth(float azimuth) {
|
||||
this.targetAzimuth = azimuth;
|
||||
// Проверяем на валидность азимута
|
||||
if (Float.isNaN(azimuth) || Float.isInfinite(azimuth)) {
|
||||
return; // Игнорируем невалидные значения
|
||||
}
|
||||
|
||||
// Нормализуем входящий азимут
|
||||
this.targetAzimuth = normalizeAngle(azimuth);
|
||||
|
||||
// Если текущий азимут еще не инициализирован, устанавливаем его сразу
|
||||
if (currentAzimuth == 0 && targetAzimuth != 0) {
|
||||
currentAzimuth = targetAzimuth;
|
||||
}
|
||||
|
||||
// Специальная обработка для 0° - если текущий азимут близок к 360°,
|
||||
// то 0° должен интерпретироваться как 360°
|
||||
if (targetAzimuth == 0 && currentAzimuth > 350) {
|
||||
this.targetAzimuth = 360;
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ public class CoordinatesDockWidget extends BaseDockWidget {
|
||||
testPaint.setTextSize(dp(16));
|
||||
testPaint.setTypeface(android.graphics.Typeface.DEFAULT_BOLD);
|
||||
testPaint.setAntiAlias(true);
|
||||
canvas.drawText("КООРДИНАТЫ", dp(16), dp(20), testPaint);
|
||||
// canvas.drawText("КООРДИНАТЫ", dp(16), dp(20), testPaint);
|
||||
|
||||
// Рисуем текст
|
||||
canvas.drawText(coordinatesText, dp(16), startY, coordinatesPaint);
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package com.grigowashere.aismap.view;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.grigowashere.aismap.R;
|
||||
import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
|
||||
/**
|
||||
* Overlay для отображения курсора на карте с координатами и информацией о расстоянии
|
||||
*/
|
||||
public class CursorOverlay {
|
||||
|
||||
private Context context;
|
||||
private View overlayView;
|
||||
private TextView tvCursorLatitude;
|
||||
private TextView tvCursorLongitude;
|
||||
private TextView tvDistance;
|
||||
private TextView tvBearing;
|
||||
private LinearLayout coordinatesPanel;
|
||||
private LinearLayout distanceBearingPanel;
|
||||
private LinearLayout aisVesselInfoPanel;
|
||||
|
||||
// AIS vessel info TextViews
|
||||
private TextView tvAisMmsi;
|
||||
private TextView tvAisName;
|
||||
private TextView tvAisCallSign;
|
||||
private TextView tvAisCog;
|
||||
private TextView tvAisSog;
|
||||
|
||||
private Vessel ownVessel;
|
||||
private AISVessel currentAisVessel;
|
||||
private double cursorLatitude;
|
||||
private double cursorLongitude;
|
||||
|
||||
public CursorOverlay(Context context) {
|
||||
this.context = context;
|
||||
initializeViews();
|
||||
}
|
||||
|
||||
private void initializeViews() {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
overlayView = inflater.inflate(R.layout.cursor, null);
|
||||
|
||||
tvCursorLatitude = overlayView.findViewById(R.id.tv_cursor_latitude);
|
||||
tvCursorLongitude = overlayView.findViewById(R.id.tv_cursor_longitude);
|
||||
tvDistance = overlayView.findViewById(R.id.tv_distance);
|
||||
tvBearing = overlayView.findViewById(R.id.tv_bearing);
|
||||
coordinatesPanel = overlayView.findViewById(R.id.coordinates_panel);
|
||||
distanceBearingPanel = overlayView.findViewById(R.id.distance_bearing_panel);
|
||||
aisVesselInfoPanel = overlayView.findViewById(R.id.ais_vessel_info_panel);
|
||||
|
||||
// Initialize AIS vessel info TextViews
|
||||
tvAisMmsi = overlayView.findViewById(R.id.tv_ais_mmsi);
|
||||
tvAisName = overlayView.findViewById(R.id.tv_ais_name);
|
||||
tvAisCallSign = overlayView.findViewById(R.id.tv_ais_call_sign);
|
||||
tvAisCog = overlayView.findViewById(R.id.tv_ais_cog);
|
||||
tvAisSog = overlayView.findViewById(R.id.tv_ais_sog);
|
||||
|
||||
// По умолчанию курсор скрыт
|
||||
overlayView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public View getView() {
|
||||
return overlayView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет координаты курсора (центра экрана)
|
||||
*/
|
||||
public void updateCursorCoordinates(double latitude, double longitude) {
|
||||
this.cursorLatitude = latitude;
|
||||
this.cursorLongitude = longitude;
|
||||
|
||||
tvCursorLatitude.setText(String.format("%.6f°", latitude));
|
||||
tvCursorLongitude.setText(String.format("%.6f°", longitude));
|
||||
|
||||
// Обновляем информацию о расстоянии и пеленге, если есть данные о нашем судне
|
||||
updateDistanceAndBearing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает данные о нашем судне для расчета расстояния и пеленга
|
||||
*/
|
||||
public void setOwnVessel(Vessel vessel) {
|
||||
this.ownVessel = vessel;
|
||||
updateDistanceAndBearing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет информацию о расстоянии и пеленге
|
||||
*/
|
||||
private void updateDistanceAndBearing() {
|
||||
if (ownVessel != null && isValidPosition(ownVessel)) {
|
||||
double distance = calculateDistance(
|
||||
ownVessel.getLatitude(), ownVessel.getLongitude(),
|
||||
cursorLatitude, cursorLongitude
|
||||
);
|
||||
|
||||
// Вычисляем пеленг от судна к курсору
|
||||
double bearingToCursor = calculateBearing(
|
||||
ownVessel.getLatitude(), ownVessel.getLongitude(),
|
||||
cursorLatitude, cursorLongitude
|
||||
);
|
||||
|
||||
// Вычисляем относительный пеленг (на сколько градусов повернуть от курса судна)
|
||||
double relativeBearing;
|
||||
if (ownVessel.getCourse() > 0) {
|
||||
// Пеленг относительно курса судна
|
||||
relativeBearing = bearingToCursor - ownVessel.getCourse();
|
||||
// Нормализуем в диапазон -180..+180
|
||||
while (relativeBearing > 180) relativeBearing -= 360;
|
||||
while (relativeBearing < -180) relativeBearing += 360;
|
||||
} else {
|
||||
// Если курс неизвестен, показываем абсолютный пеленг
|
||||
relativeBearing = bearingToCursor;
|
||||
}
|
||||
|
||||
// Форматируем расстояние: в км с дробной частью если > 1000м, иначе в метрах
|
||||
String distanceText;
|
||||
if (distance >= 1000) {
|
||||
distanceText = String.format("Rng: %.2f км", distance / 1000.0);
|
||||
} else {
|
||||
distanceText = String.format("Rng: %.1f м", distance);
|
||||
}
|
||||
|
||||
tvDistance.setText(distanceText);
|
||||
tvBearing.setText(String.format("Brg: %.1f°", relativeBearing));
|
||||
|
||||
// Показываем информацию о расстоянии и пеленге
|
||||
tvDistance.setVisibility(View.VISIBLE);
|
||||
tvBearing.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// Скрываем информацию, если нет валидных координат нашего судна
|
||||
tvDistance.setVisibility(View.GONE);
|
||||
tvBearing.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет расстояние между двумя точками в метрах (формула гаверсинуса)
|
||||
*/
|
||||
private double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
|
||||
final int R = 6371000; // Радиус Земли в метрах
|
||||
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double deltaLatRad = Math.toRadians(lat2 - lat1);
|
||||
double deltaLonRad = Math.toRadians(lon2 - lon1);
|
||||
|
||||
double a = Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) +
|
||||
Math.cos(lat1Rad) * Math.cos(lat2Rad) *
|
||||
Math.sin(deltaLonRad / 2) * Math.sin(deltaLonRad / 2);
|
||||
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||
|
||||
return R * c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Вычисляет пеленг от первой точки ко второй в градусах
|
||||
*/
|
||||
private double calculateBearing(double lat1, double lon1, double lat2, double lon2) {
|
||||
double lat1Rad = Math.toRadians(lat1);
|
||||
double lat2Rad = Math.toRadians(lat2);
|
||||
double deltaLonRad = Math.toRadians(lon2 - lon1);
|
||||
|
||||
double y = Math.sin(deltaLonRad) * Math.cos(lat2Rad);
|
||||
double x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
|
||||
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLonRad);
|
||||
|
||||
double bearing = Math.toDegrees(Math.atan2(y, x));
|
||||
return (bearing + 360) % 360; // Нормализуем в диапазон 0-360
|
||||
}
|
||||
|
||||
/**
|
||||
* Скрывает курсор
|
||||
*/
|
||||
public void hideCursor() {
|
||||
if (overlayView != null) {
|
||||
overlayView.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показывает курсор
|
||||
*/
|
||||
public void showCursor() {
|
||||
if (overlayView != null) {
|
||||
overlayView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает информацию об AIS судне под курсором
|
||||
*/
|
||||
public void setAisVesselInfo(AISVessel vessel) {
|
||||
this.currentAisVessel = vessel;
|
||||
updateAisVesselInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет отображение информации об AIS судне
|
||||
*/
|
||||
private void updateAisVesselInfo() {
|
||||
if (currentAisVessel != null) {
|
||||
// MMSI
|
||||
tvAisMmsi.setText("MMSI: " + currentAisVessel.getMmsi());
|
||||
|
||||
// Название
|
||||
String name = currentAisVessel.getVesselName();
|
||||
if (name != null && !name.trim().isEmpty()) {
|
||||
tvAisName.setText("Название: " + name);
|
||||
tvAisName.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvAisName.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Позывной
|
||||
String callSign = currentAisVessel.getCallSign();
|
||||
if (callSign != null && !callSign.trim().isEmpty()) {
|
||||
tvAisCallSign.setText("Позывной: " + callSign);
|
||||
tvAisCallSign.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvAisCallSign.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// COG (курс)
|
||||
if (currentAisVessel.getCourse() > 0) {
|
||||
tvAisCog.setText(String.format("COG: %.1f°", currentAisVessel.getCourse()));
|
||||
tvAisCog.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvAisCog.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// SOG (скорость)
|
||||
if (currentAisVessel.getSpeed() > 0) {
|
||||
tvAisSog.setText(String.format("SOG: %.1f уз", currentAisVessel.getSpeed()));
|
||||
tvAisSog.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
tvAisSog.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Показываем панель
|
||||
aisVesselInfoPanel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// Скрываем панель
|
||||
aisVesselInfoPanel.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Очищает информацию об AIS судне
|
||||
*/
|
||||
public void clearAisVesselInfo() {
|
||||
this.currentAisVessel = null;
|
||||
aisVesselInfoPanel.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет валидность позиции судна
|
||||
*/
|
||||
private boolean isValidPosition(Vessel vessel) {
|
||||
if (vessel == null) return false;
|
||||
|
||||
double lat = vessel.getLatitude();
|
||||
double lon = vessel.getLongitude();
|
||||
|
||||
// Проверяем, что координаты в допустимых пределах
|
||||
return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180 &&
|
||||
lat != 0.0 && lon != 0.0; // Исключаем нулевые координаты
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="62.13dp"
|
||||
android:height="77.5dp"
|
||||
android:viewportWidth="62.13"
|
||||
android:viewportHeight="77.5">
|
||||
<path
|
||||
android:pathData="M31.09,13m-9.5,0a9.5,9.5 0,1 1,19 0a9.5,9.5 0,1 1,-19 0"
|
||||
android:strokeWidth="7"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M3.09,45.5l4.79,9.05c14.4,27.2 34.78,25.73 48.48,-3.49l2.6,-5.56"
|
||||
android:strokeWidth="7"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M31.09,22.5L31.09,71"
|
||||
android:strokeWidth="7"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,26 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="129dp"
|
||||
android:height="129dp"
|
||||
android:viewportWidth="129"
|
||||
android:viewportHeight="129">
|
||||
<path
|
||||
android:pathData="M62.39,72.13L12.02,122.94c-1.86,1.87 -0.53,5.06 2.11,5.06h100.75c2.64,0 3.97,-3.19 2.11,-5.06l-50.38,-50.81c-1.16,-1.17 -3.06,-1.17 -4.22,0Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M56.87,62.39L6.06,12.02c-1.87,-1.86 -5.06,-0.53 -5.06,2.11v100.75c0,2.64 3.19,3.97 5.06,2.11l50.81,-50.38c1.17,-1.16 1.17,-3.06 0,-4.22Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M66.61,56.87L116.98,6.06c1.86,-1.87 0.53,-5.06 -2.11,-5.06H14.12c-2.64,0 -3.97,3.19 -2.11,5.06l50.38,50.81c1.16,1.17 3.06,1.17 4.22,0Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M72.13,66.61l50.81,50.38c1.87,1.86 5.06,0.53 5.06,-2.11V14.12c0,-2.64 -3.19,-3.97 -5.06,-2.11l-50.81,50.38c-1.17,1.16 -1.17,3.06 0,4.22Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,78 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="232.51dp"
|
||||
android:height="232.77dp"
|
||||
android:viewportWidth="232.51"
|
||||
android:viewportHeight="232.77">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M153.63,0l0,0l78.88,0l0,79.69l-78.88,0l0,-79.69z"/>
|
||||
<path
|
||||
android:pathData="M153.63,4.5L232.51,4.5"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M153.63,0l0,0l78.88,0l0,79.69l-78.88,0l0,-79.69z"/>
|
||||
<path
|
||||
android:pathData="M227.77,0.82L227.77,79.69"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M153.63,232.77l0,0l78.88,0l0,-79.69l-78.88,0l0,79.69z"/>
|
||||
<path
|
||||
android:pathData="M153.63,228.27L232.51,228.27"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M153.63,232.77l0,0l78.88,0l0,-79.69l-78.88,0l0,79.69z"/>
|
||||
<path
|
||||
android:pathData="M227.77,231.95L227.77,153.08"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,79.69l0,0l78.87,0l0,-79.69l-78.87,0l0,79.69z"/>
|
||||
<path
|
||||
android:pathData="M78.87,4.5L0,4.5"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,79.69l0,0l78.87,0l0,-79.69l-78.87,0l0,79.69z"/>
|
||||
<path
|
||||
android:pathData="M4.73,0.82L4.73,79.69"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,153.08l0,0l78.87,0l0,79.69l-78.87,0l0,-79.69z"/>
|
||||
<path
|
||||
android:pathData="M78.87,228.27L0,228.27"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,153.08l0,0l78.87,0l0,79.69l-78.87,0l0,-79.69z"/>
|
||||
<path
|
||||
android:pathData="M4.73,231.95L4.73,153.08"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<!-- Горизонтальная линия креста -->
|
||||
<item android:top="18dp" android:bottom="18dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FF0000" />
|
||||
<size android:width="40dp" android:height="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Вертикальная линия креста -->
|
||||
<item android:left="18dp" android:right="18dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#FF0000" />
|
||||
<size android:width="4dp" android:height="40dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
<!-- Центральная точка -->
|
||||
<item android:left="18dp" android:right="18dp" android:top="18dp" android:bottom="18dp">
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="#FFFFFF" />
|
||||
<size android:width="4dp" android:height="4dp" />
|
||||
</shape>
|
||||
</item>
|
||||
|
||||
</layer-list>
|
||||
@@ -0,0 +1,62 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="137.43dp"
|
||||
android:height="137.43dp"
|
||||
android:viewportWidth="137.43"
|
||||
android:viewportHeight="137.43">
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M69.22,68.97m-15.5,0a15.5,15.5 0,1 1,31 0a15.5,15.5 0,1 1,-31 0"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M50.01,71.14l-6,-0.28l-0.02,-4.4l6,-0.32l0.02,5z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M31.34,70.27l-12.67,-0.59l-0.01,-1.86l12.67,-0.68l0.01,3.13z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M6,69.09l-6,-0.27l6,-0.33l0,0.6z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M66.29,50.01l0.28,-6l4.4,-0.02l0.32,6l-5,0.02z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M67.16,31.34l0.59,-12.67l1.87,-0.01l0.68,12.67l-3.14,0.01z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M68.34,6l0.28,-6l0.32,6l-0.6,0z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M87.43,66.29l6,0.28l0.01,4.4l-6,0.32l-0.01,-5z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M106.1,67.16l12.66,0.59l0.01,1.87l-12.66,0.68l-0.01,-3.14z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M131.43,68.34l6,0.28l-5.99,0.32l-0.01,-0.6z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M71.14,87.43l-0.28,6l-4.4,0.01l-0.32,-6l5,-0.01z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M70.27,106.1l-0.59,12.66l-1.86,0.01l-0.68,-12.66l3.13,-0.01z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M69.09,131.43l-0.27,6l-0.33,-5.99l0.6,-0.01z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M69.26,70.79c1.93,0 1.93,-3 0,-3s-1.93,3 0,3h0Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M80.01,134.2c-0.42,-4.29 -0.71,-8.58 -1.01,-12.86 0.01,-6.42 0,-19.31 0,-25.73 0,0 0,-3.22 0,-3.22v-1.61c-0.03,-2.95 1.19,-5.88 3.31,-7.94 1.69,-1.64 3.92,-2.74 6.27,-3.02 2.05,-0.21 4.54,-0.04 6.55,-0.09 0,0 12.86,0 12.86,0h12.86c4.29,0.34 8.58,0.59 12.86,1 -4.29,0.41 -8.58,0.66 -12.86,1 -6.42,0 -19.31,0 -25.73,0 -1.77,0.05 -4.64,-0.1 -6.3,0.08 -1.92,0.23 -3.73,1.12 -5.12,2.47 -1.74,1.7 -2.72,4.08 -2.7,6.51 0,0 0,1.61 0,1.61 0,0 0,3.22 0,3.22 0,6.41 0,19.32 0,25.73 -0.29,4.29 -0.59,8.58 -1.01,12.86h0Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M4.26,79.75c4.29,-0.42 8.58,-0.71 12.86,-1.01 6.42,0.01 19.31,0 25.73,0 0,0 3.22,0 3.22,0h1.61c2.95,-0.03 5.88,1.19 7.94,3.31 1.64,1.69 2.74,3.92 3.02,6.27 0.21,2.05 0.04,4.54 0.09,6.55 0,0 0,12.86 0,12.86v12.86c-0.34,4.29 -0.59,8.58 -1,12.86 -0.41,-4.29 -0.66,-8.58 -1,-12.86 0,-6.42 0,-19.31 0,-25.73 -0.05,-1.77 0.1,-4.64 -0.08,-6.3 -0.23,-1.92 -1.12,-3.73 -2.47,-5.12 -1.7,-1.74 -4.08,-2.72 -6.51,-2.7 0,0 -1.61,0 -1.61,0 0,0 -3.22,0 -3.22,0 -6.41,0 -19.32,0 -25.73,0 -4.29,-0.29 -8.58,-0.59 -12.86,-1.01h0Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M58.72,4c0.42,4.29 0.71,8.58 1.01,12.86 -0.01,6.42 0,19.31 0,25.73 0,0 0,3.22 0,3.22v1.61c0.03,2.95 -1.19,5.88 -3.31,7.94 -1.69,1.64 -3.92,2.74 -6.27,3.02 -2.05,0.21 -4.54,0.04 -6.55,0.09 0,0 -12.86,0 -12.86,0h-12.86c-4.29,-0.34 -8.58,-0.59 -12.86,-1 4.29,-0.41 8.58,-0.66 12.86,-1 6.42,0 19.31,0 25.73,0 1.77,-0.05 4.64,0.1 6.3,-0.08 1.92,-0.23 3.73,-1.12 5.12,-2.47 1.74,-1.7 2.72,-4.08 2.7,-6.51 0,0 0,-1.61 0,-1.61 0,0 0,-3.22 0,-3.22 0,-6.41 0,-19.32 0,-25.73 0.29,-4.29 0.59,-8.58 1.01,-12.86h0Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M134.46,58.46c-4.29,0.42 -8.58,0.71 -12.86,1.01 -6.42,-0.01 -19.31,0 -25.73,0 0,0 -3.22,0 -3.22,0h-1.61c-2.95,0.03 -5.88,-1.19 -7.94,-3.31 -1.64,-1.69 -2.74,-3.92 -3.02,-6.27 -0.21,-2.05 -0.04,-4.54 -0.09,-6.55 0,0 0,-12.86 0,-12.86v-12.86c0.34,-4.29 0.59,-8.58 1,-12.86 0.41,4.29 0.66,8.58 1,12.86 0,6.42 0,19.31 0,25.73 0.05,1.77 -0.1,4.64 0.08,6.3 0.23,1.92 1.12,3.73 2.47,5.12 1.7,1.74 4.08,2.72 6.51,2.7 0,0 1.61,0 1.61,0 0,0 3.22,0 3.22,0 6.41,0 19.32,0 25.73,0 4.29,0.29 8.58,0.59 12.86,1.01h0Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,61 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="62.24dp"
|
||||
android:height="62.18dp"
|
||||
android:viewportWidth="62.24"
|
||||
android:viewportHeight="62.18">
|
||||
<path
|
||||
android:pathData="M30.66,31.22m-6,0a6,6 0,1 1,12 0a6,6 0,1 1,-12 0"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M27.25,41.64l-9.4,7.49c-2.27,1.81 -5.4,2.07 -7.94,0.66l-1.42,-0.78c-2.91,-1.61 -2.83,-5.83 0.15,-7.32l7.01,-3.52 11.59,3.47Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M19.94,31.19l-10.03,-6.62c-2.42,-1.6 -3.64,-4.5 -3.08,-7.34l0.31,-1.59c0.63,-3.27 4.67,-4.49 7.01,-2.12l5.51,5.58 0.28,12.1Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M27.61,21l3.2,-11.58c0.77,-2.79 3.15,-4.85 6.03,-5.2l1.61,-0.2c3.3,-0.41 5.71,3.05 4.18,6.01l-3.6,6.97 -11.42,4.01Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M39.68,25.15l12.01,-0.54c2.9,-0.13 5.59,1.5 6.81,4.13l0.68,1.47c1.41,3.02 -1.14,6.37 -4.42,5.83l-7.74,-1.27 -7.34,-9.62Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M39.46,37.91l4.22,11.25c1.02,2.71 0.3,5.78 -1.82,7.75l-1.18,1.1c-2.43,2.27 -6.41,0.89 -6.92,-2.4l-1.18,-7.75 6.88,-9.95Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M57.93,41.9l-2.71,4.16s3.31,-2.54 2.71,-4.16Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M29.15,59.9l-4.8,-1.29s3.44,2.36 4.8,1.29Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M3.13,38.09l-0.25,-4.96s-1.19,4 0.25,4.96Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M15.84,6.6l4.64,-1.77s-4.17,0.11 -4.64,1.77Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M49.71,8.96l3.12,3.87s-1.39,-3.93 -3.12,-3.87Z"
|
||||
android:strokeWidth="4"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="45.83dp"
|
||||
android:height="65.94dp"
|
||||
android:viewportWidth="45.83"
|
||||
android:viewportHeight="65.94">
|
||||
<path
|
||||
android:pathData="M41.75,62.87V4.78l-3.87,2.32 -6.48,3.89c-9.59,5.76 -17.51,13.94 -22.94,23.72l-6.71,12.07"
|
||||
android:fillColor="#00FFFFFF"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M41.56,62.87c0,-16.54 0.05,-34.31 -0.59,-50.83 -0.1,-2.42 -0.17,-4.84 -1.23,-7.26 0,0 3.03,1.71 3.03,1.71 -3.66,2.26 -9.09,5.36 -12.6,7.63 -7.99,5.26 -14.73,12.41 -19.49,20.71 0,0 -7.19,12.91 -7.19,12.91 0,0 -3.5,-1.94 -3.5,-1.94 0,0 7.22,-12.97 7.22,-12.97 5.07,-8.83 12.25,-16.45 20.75,-22.05 5.53,-3.51 12.26,-7.38 17.85,-10.79 0,0 -2.08,4.78 -2.08,4.78 -1.8,4.72 -1.15,9.64 -1.49,14.52 -0.19,8.41 -0.24,20.51 -0.33,29.04 -0.02,4.84 -0.03,9.68 0.01,14.52h-0.37Z"/>
|
||||
<path
|
||||
android:pathData="M41.47,62.28m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
||||
android:strokeWidth=".5"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M41.47,63.28v1.8h0c-0.51,0.74 -1.58,0.81 -2.18,0.14l-0.27,-0.31"
|
||||
android:strokeWidth=".5"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,26 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="107dp"
|
||||
android:height="127.1dp"
|
||||
android:viewportWidth="107"
|
||||
android:viewportHeight="127.1">
|
||||
<path
|
||||
android:pathData="M0,115.1L43,115.1"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M64,115.1L107,115.1"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M95,8.1l-12.08,-6.7 -35.92,80.7L15,115.1s11.69,-0.03 28,0c0,0 1,-10 11,-10 9,0 10,10 10,10 9.54,0.02 15.06,0 15,0l-5,-29L95,8.1Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00ff00"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M53.5,115.6m-10.5,0a10.5,10.5 0,1 1,21 0a10.5,10.5 0,1 1,-21 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="239.36dp"
|
||||
android:height="239.36dp"
|
||||
android:viewportWidth="239.36"
|
||||
android:viewportHeight="239.36">
|
||||
<path
|
||||
android:pathData="M3.18,3.18L236.18,236.18"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M236.18,3.18L3.18,236.18"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,26 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="37.67dp"
|
||||
android:height="44.99dp"
|
||||
android:viewportWidth="37.67"
|
||||
android:viewportHeight="44.99">
|
||||
<path
|
||||
android:pathData="M6.71,43.99V8.86H2.32l6.3,-6.67c0.7,-0.74 1.66,-1.16 2.68,-1.16l14.07,-0.04c1.5,0 2.93,0.6 3.97,1.68l5.97,6.18h-4.6l1,35.13H6.71Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M4.21,16.49L33.21,20.49"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M4.21,20.49L33.21,24.12"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M4.21,24.12L33.21,27.84"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#80000000" />
|
||||
<corners android:radius="8dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="#40FFFFFF" />
|
||||
|
||||
</shape>
|
||||
@@ -0,0 +1,26 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="107dp"
|
||||
android:height="127.1dp"
|
||||
android:viewportWidth="107"
|
||||
android:viewportHeight="127.1">
|
||||
<path
|
||||
android:pathData="M0,115.1L43,115.1"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M64,115.1L107,115.1"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M95,8.1l-12.08,-6.7 -35.92,80.7L15,115.1s11.69,-0.03 28,0c0,0 1,-10 11,-10 9,0 10,10 10,10 9.54,0.02 15.06,0 15,0l-5,-29L95,8.1Z"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ff0000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M53.5,115.6m-10.5,0a10.5,10.5 0,1 1,21 0a10.5,10.5 0,1 1,-21 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,26 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50.57dp"
|
||||
android:height="51.98dp"
|
||||
android:viewportWidth="50.57"
|
||||
android:viewportHeight="51.98">
|
||||
<path
|
||||
android:pathData="M21.49,2.23l0,20.35l0.3,29.39"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M0.79,41.97L50.57,41.97"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#fff"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M25.68,2.23l0,35.8l23.13,0l-23.13,-35.8z"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:strokeWidth="1"
|
||||
android:pathData="M17.7,2.23l0,35.8l-16.91,0l16.91,-35.8z"
|
||||
android:fillColor="#00FFFFFF"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="165.04dp"
|
||||
android:height="282dp"
|
||||
android:viewportWidth="165.04"
|
||||
android:viewportHeight="282">
|
||||
<path
|
||||
android:pathData="M120.41,4.5l-77.27,0l-38.64,88.31l0,184.69l156,0l-1.46,-184.69l-38.63,-88.31z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#B5B8B1"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#D9913C"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#3A6EA5"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#1D3557"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#B5B8B1"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#2BA9E0"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#35C2A9"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#C23B22"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#E7621B"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#D9913C"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#3A6EA5"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#1D3557"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#B5B8B1"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#2BA9E0"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#35C2A9"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#C23B22"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="91.38dp"
|
||||
android:height="162.6dp"
|
||||
android:viewportWidth="91.38"
|
||||
android:viewportHeight="162.6">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||
android:strokeWidth="12"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#E7621B"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,18 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="88.5dp"
|
||||
android:height="152.73dp"
|
||||
android:viewportWidth="88.5"
|
||||
android:viewportHeight="152.73">
|
||||
<!-- Внешняя обводка для контраста -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="9"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
<!-- Основная форма с внутренней обводкой -->
|
||||
<path
|
||||
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||
android:strokeWidth="3"
|
||||
android:fillColor="#B5B8B1"
|
||||
android:strokeColor="#FFFFFF"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_target_count"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="AIS цели: 0"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:padding="12dp"
|
||||
android:background="#f0f0f0"
|
||||
android:textColor="#333333"
|
||||
android:gravity="center" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_ais_targets"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:text="Нет AIS целей\nВсе корабли уплыли"
|
||||
android:textAlignment="center"
|
||||
android:textSize="18sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<!-- Карта -->
|
||||
<com.yandex.mapkit.mapview.MapView
|
||||
<org.maplibre.android.maps.MapView
|
||||
android:id="@+id/map_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
@@ -54,6 +54,36 @@
|
||||
android:minWidth="120dp"
|
||||
android:background="@android:color/white" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_ais_targets"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Цели AIS"
|
||||
android:textSize="12sp"
|
||||
android:minWidth="120dp"
|
||||
android:background="@android:color/white"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
|
||||
<!-- Строки возраста последних сообщений GPS ($) и AIS (!) -->
|
||||
<TextView
|
||||
android:id="@+id/tv_gps_age"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="GPS: --"
|
||||
android:textSize="11sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_marginTop="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_ais_age"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="AIS: --"
|
||||
android:textSize="11sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_marginTop="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Компас -->
|
||||
|
||||
@@ -72,6 +72,143 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Треки и предсказание -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🧭 Путь и предсказание"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Максимум точек на судно"
|
||||
app:helperText="Ограничение размера истории пути">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_path_max_points"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="300" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Толщина линии пути (px)">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_path_width"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal"
|
||||
android:text="3.0" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Цвет пути (#RRGGBB)">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_path_color"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:text="#00FFFF" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Толщина линии предсказания (px)">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_prediction_width"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="numberDecimal"
|
||||
android:text="2.0" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Цвет предсказания (#RRGGBB)">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_prediction_color"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text"
|
||||
android:text="#FFFF00" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:hint="Горизонт предсказания (сек)">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_prediction_horizon_sec"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="60" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<!-- Кнопка очистки трекера пути -->
|
||||
<Button
|
||||
android:id="@+id/btn_clear_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🗑️ Очистить трекер пути"
|
||||
android:textSize="16sp"
|
||||
android:layout_marginTop="8dp"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Удаляет все сохраненные точки пути собственного судна"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Приоритеты данных -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
@@ -209,6 +346,258 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Настройки устаревания данных -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="⏰ Устаревание данных AIS"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Настройте время, через которое данные о судах считаются устаревшими:"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Время предупреждения (минуты)"
|
||||
app:helperText="Судна старше этого времени будут помечены как устаревшие">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_stale_warning_minutes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="5" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:hint="Время удаления (минуты)"
|
||||
app:helperText="Судна старше этого времени будут удалены с карты">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_stale_remove_minutes"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:text="7" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="💡 Совет: Устаревшие суда отображаются с иконкой losingtarget.xml"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginTop="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Настройки экрана -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="📱 Управление экраном"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Настройте поведение экрана во время навигации:"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switch_keep_screen_on"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Не давать экрану засыпать"
|
||||
android:textSize="16sp"
|
||||
android:checked="true"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Экран будет оставаться включенным во время навигации (рекомендуется для навигатора)"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Настройки уведомлений -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🔔 Уведомления о новых целях AIS"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Настройте уведомления при обнаружении новых судов:"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switch_vibration_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Вибрация"
|
||||
android:textSize="16sp"
|
||||
android:checked="true"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Вибрация устройства при обнаружении нового судна"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switch_sound_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Звуковое уведомление"
|
||||
android:textSize="16sp"
|
||||
android:checked="true"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Звуковой сигнал при обнаружении нового судна"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Настройки курсора -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🎯 Курсор на карте"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Настройте отображение курсора с координатами центра экрана:"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||
android:id="@+id/switch_cursor_enabled"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Показать курсор"
|
||||
android:textSize="16sp"
|
||||
android:checked="false"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Отображать крест в центре экрана с координатами и информацией о расстоянии"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginStart="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Кнопки -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -46,6 +46,17 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Время назад -->
|
||||
<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" />
|
||||
<!-- MMSI -->
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_ais_mmsi"
|
||||
@@ -58,17 +69,6 @@
|
||||
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
|
||||
@@ -119,17 +119,52 @@
|
||||
android:padding="8dp" />
|
||||
|
||||
<!-- Курс -->
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_ais_course"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🧭 Курс: --°"
|
||||
android:orientation="horizontal">
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_ais_course"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="🧭 COG: --°"
|
||||
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_heading"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="🧭 HDG: --°"
|
||||
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_rot"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="🔄 ROT: --°/мин"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/black"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="8dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Скорость -->
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_ais_speed"
|
||||
@@ -226,6 +261,30 @@
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="8dp" />
|
||||
|
||||
<!-- Расстояние до судна -->
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_ais_distance"
|
||||
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_bearing"
|
||||
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"
|
||||
@@ -238,17 +297,7 @@
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent">
|
||||
|
||||
<!-- Курсор в виде креста в центре экрана -->
|
||||
<View
|
||||
android:id="@+id/cursor_cross"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="@drawable/cursorcross" />
|
||||
|
||||
<!-- Координаты в первом квадранте (верхний левый) -->
|
||||
|
||||
<!-- Расстояние и пеленг в четвертом квадранте (нижний правый) -->
|
||||
<LinearLayout
|
||||
android:id="@+id/coordinates_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:background="@drawable/panel_background"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:translationX="-60dp"
|
||||
android:translationY="-40dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cursor_latitude"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Широта: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cursor_longitude"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Долгота: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/distance_bearing_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginStart="120dp"
|
||||
android:layout_marginTop="60dp"
|
||||
android:background="@drawable/panel_background"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:translationX="60dp"
|
||||
android:translationY="40dp"
|
||||
android:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_distance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Rnd:"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_bearing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Brg: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Информация об AIS судне в левом нижнем углу -->
|
||||
<LinearLayout
|
||||
android:id="@+id/ais_vessel_info_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:background="@drawable/panel_background"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:translationX="-75dp"
|
||||
android:translationY="60dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_ais_mmsi"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="MMSI: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_ais_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Название: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_ais_call_sign"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="Позывной: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_ais_cog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="COG: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_ais_sog"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="monospace"
|
||||
android:text="SOG: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:textSize="16sp"
|
||||
android:text="Vessel" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_mmsi"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:text="MMSI" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_coords"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:text="0, 0" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_course_speed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:text="COG 0 • 0 kn" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_last_update"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:text="Обновлено: --" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time_ago"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:text="N сек назад" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_distance"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:text="Расстояние: --"
|
||||
android:textColor="#666666" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_bearing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="13sp"
|
||||
android:text="Азимут: --"
|
||||
android:textColor="#666666" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="8dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_marine_traffic"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="MarineTraffic" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_center_on_map"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="На карте"
|
||||
android:layout_marginStart="12dp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -20,4 +20,22 @@
|
||||
android:icon="@android:drawable/ic_menu_delete"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_path_tracking"
|
||||
android:title="Пути"
|
||||
android:icon="@android:drawable/ic_menu_directions"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_service_test"
|
||||
android:title="Тест сервиса"
|
||||
android:icon="@android:drawable/ic_menu_manage"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_keep_screen_on"
|
||||
android:title="Экран"
|
||||
android:icon="@android:drawable/ic_menu_view"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
warning: in the working copy of '.idea/misc.xml', LF will be replaced by CRLF the next time Git touches it
|
||||
warning: in the working copy of 'app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java', LF will be replaced by CRLF the next time Git touches it
|
||||
.idea/deploymentTargetSelector.xml
|
||||
.idea/vcs.xml
|
||||
app/build.gradle
|
||||
app/src/main/AndroidManifest.xml
|
||||
app/src/main/java/com/grigowashere/aismap/MainActivity.java
|
||||
app/src/main/java/com/grigowashere/aismap/SettingsActivity.java
|
||||
app/src/main/java/com/grigowashere/aismap/controllers/AppController.java
|
||||
app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java
|
||||
app/src/main/java/com/grigowashere/aismap/maps/MapForgeImpl.java
|
||||
app/src/main/java/com/grigowashere/aismap/maps/MarkerManager.java
|
||||
app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java
|
||||
app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java
|
||||
app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java
|
||||
app/src/main/java/com/grigowashere/aismap/models/AISVessel.java
|
||||
app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java
|
||||
app/src/main/res/drawable/target.xml
|
||||
app/src/main/res/drawable/targetclassa.xml
|
||||
app/src/main/res/layout/activity_main.xml
|
||||
app/src/main/res/layout/activity_settings.xml
|
||||
app/src/main/res/layout/bottom_sheet_ais_vessel.xml
|
||||
app/src/main/res/menu/main_menu.xml
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.24 62.18">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_9" data-name="Слой_9">
|
||||
<circle class="cls-2" cx="30.66" cy="31.22" r="6"/>
|
||||
<g>
|
||||
<path class="cls-2" d="M27.25,41.64l-9.4,7.49c-2.27,1.81-5.4,2.07-7.94.66l-1.42-.78c-2.91-1.61-2.83-5.83.15-7.32l7.01-3.52,11.59,3.47Z"/>
|
||||
<path class="cls-2" d="M19.94,31.19l-10.03-6.62c-2.42-1.6-3.64-4.5-3.08-7.34l.31-1.59c.63-3.27,4.67-4.49,7.01-2.12l5.51,5.58.28,12.1Z"/>
|
||||
<path class="cls-2" d="M27.61,21l3.2-11.58c.77-2.79,3.15-4.85,6.03-5.2l1.61-.2c3.3-.41,5.71,3.05,4.18,6.01l-3.6,6.97-11.42,4.01Z"/>
|
||||
<path class="cls-2" d="M39.68,25.15l12.01-.54c2.9-.13,5.59,1.5,6.81,4.13l.68,1.47c1.41,3.02-1.14,6.37-4.42,5.83l-7.74-1.27-7.34-9.62Z"/>
|
||||
<path class="cls-2" d="M39.46,37.91l4.22,11.25c1.02,2.71.3,5.78-1.82,7.75l-1.18,1.1c-2.43,2.27-6.41.89-6.92-2.4l-1.18-7.75,6.88-9.95Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M57.93,41.9l-2.71,4.16s3.31-2.54,2.71-4.16Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-2" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M29.15,59.9l-4.8-1.29s3.44,2.36,4.8,1.29Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-3" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M3.13,38.09l-.25-4.96s-1.19,4,.25,4.96Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-4" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M15.84,6.6l4.64-1.77s-4.17.11-4.64,1.77Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-5" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M49.71,8.96l3.12,3.87s-1.39-3.93-3.12-3.87Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37.67 44.99">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_11" data-name="Слой_11">
|
||||
<path class="cls-1" d="M6.71,43.99V8.86H2.32l6.3-6.67c.7-.74,1.66-1.16,2.68-1.16l14.07-.04c1.5,0,2.93.6,3.97,1.68l5.97,6.18h-4.6l1,35.13H6.71Z"/>
|
||||
<line class="cls-1" x1="4.21" y1="16.49" x2="33.21" y2="20.49"/>
|
||||
<line class="cls-1" x1="4.21" y1="20.49" x2="33.21" y2="24.12"/>
|
||||
<line class="cls-1" x1="4.21" y1="24.12" x2="33.21" y2="27.84"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 731 B |
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.24 62.18">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2 {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_9" data-name="Слой_9">
|
||||
<circle class="cls-2" cx="30.66" cy="31.22" r="6"/>
|
||||
<g>
|
||||
<path class="cls-2" d="M27.25,41.64l-9.4,7.49c-2.27,1.81-5.4,2.07-7.94.66l-1.42-.78c-2.91-1.61-2.83-5.83.15-7.32l7.01-3.52,11.59,3.47Z"/>
|
||||
<path class="cls-2" d="M19.94,31.19l-10.03-6.62c-2.42-1.6-3.64-4.5-3.08-7.34l.31-1.59c.63-3.27,4.67-4.49,7.01-2.12l5.51,5.58.28,12.1Z"/>
|
||||
<path class="cls-2" d="M27.61,21l3.2-11.58c.77-2.79,3.15-4.85,6.03-5.2l1.61-.2c3.3-.41,5.71,3.05,4.18,6.01l-3.6,6.97-11.42,4.01Z"/>
|
||||
<path class="cls-2" d="M39.68,25.15l12.01-.54c2.9-.13,5.59,1.5,6.81,4.13l.68,1.47c1.41,3.02-1.14,6.37-4.42,5.83l-7.74-1.27-7.34-9.62Z"/>
|
||||
<path class="cls-2" d="M39.46,37.91l4.22,11.25c1.02,2.71.3,5.78-1.82,7.75l-1.18,1.1c-2.43,2.27-6.41.89-6.92-2.4l-1.18-7.75,6.88-9.95Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M57.93,41.9l-2.71,4.16s3.31-2.54,2.71-4.16Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-2" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M29.15,59.9l-4.8-1.29s3.44,2.36,4.8,1.29Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-3" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M3.13,38.09l-.25-4.96s-1.19,4,.25,4.96Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-4" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M15.84,6.6l4.64-1.77s-4.17.11-4.64,1.77Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-5" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path class="cls-1" d="M49.71,8.96l3.12,3.87s-1.39-3.93-3.12-3.87Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 37.67 44.99">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_11" data-name="Слой_11">
|
||||
<path class="cls-1" d="M6.71,43.99V8.86H2.32l6.3-6.67c.7-.74,1.66-1.16,2.68-1.16l14.07-.04c1.5,0,2.93.6,3.97,1.68l5.97,6.18h-4.6l1,35.13H6.71Z"/>
|
||||
<line class="cls-1" x1="4.21" y1="16.49" x2="33.21" y2="20.49"/>
|
||||
<line class="cls-1" x1="4.21" y1="20.49" x2="33.21" y2="24.12"/>
|
||||
<line class="cls-1" x1="4.21" y1="24.12" x2="33.21" y2="27.84"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 731 B |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 62.13 77.5">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 7px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_8" data-name="Слой_8">
|
||||
<circle class="cls-1" cx="31.09" cy="13" r="9.5"/>
|
||||
<path class="cls-1" d="M3.09,45.5l4.79,9.05c14.4,27.2,34.78,25.73,48.48-3.49l2.6-5.56"/>
|
||||
<line class="cls-1" x1="31.09" y1="22.5" x2="31.09" y2="71"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 585 B |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 236 236">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #666;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: 3px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_22" data-name="Слой_22">
|
||||
<path class="cls-1" d="M226.29,89.5h-19.67c-7.31,0-10.97-8.84-5.8-14.01l13.91-13.91c3.21-3.21,3.21-8.4,0-11.61l-28.7-28.7c-3.21-3.21-8.4-3.21-11.61,0l-13.91,13.91c-5.17,5.17-14.01,1.51-14.01-5.8V9.71c0-4.53-3.67-8.21-8.21-8.21h-40.58c-4.53,0-8.21,3.67-8.21,8.21v19.67c0,7.31-8.84,10.97-14.01,5.8l-13.91-13.91c-3.21-3.21-8.4-3.21-11.61,0l-28.7,28.7c-3.21,3.21-3.21,8.4,0,11.61l13.91,13.91c5.17,5.17,1.51,14.01-5.8,14.01H9.71c-4.53,0-8.21,3.67-8.21,8.21v40.58c0,4.53,3.67,8.21,8.21,8.21h19.67c7.31,0,10.97,8.84,5.8,14.01l-13.91,13.91c-3.21,3.21-3.21,8.4,0,11.61l28.7,28.7c3.21,3.21,8.4,3.21,11.61,0l13.91-13.91c5.17-5.17,14.01-1.51,14.01,5.8v19.67c0,4.53,3.67,8.21,8.21,8.21h40.58c4.53,0,8.21-3.67,8.21-8.21v-19.67c0-7.31,8.84-10.97,14.01-5.8l13.91,13.91c3.21,3.21,8.4,3.21,11.61,0l28.7-28.7c3.21-3.21,3.21-8.4,0-11.61l-13.91-13.91c-5.17-5.17-1.51-14.01,5.8-14.01h19.67c4.53,0,8.21-3.67,8.21-8.21v-40.58c0-4.53-3.67-8.21-8.21-8.21ZM118.5,158.5c-22.09,0-40-17.91-40-40s17.91-40,40-40,40,17.91,40,40-17.91,40-40,40Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 137.43 137.43">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_16" data-name="Слой_16">
|
||||
<circle class="cls-1" cx="69.22" cy="68.97" r="15.5"/>
|
||||
<g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_">
|
||||
<g>
|
||||
<polygon points="50.01 71.14 44.01 70.86 43.99 66.46 49.99 66.14 50.01 71.14"/>
|
||||
<polygon points="31.34 70.27 18.67 69.68 18.66 67.82 31.33 67.14 31.34 70.27"/>
|
||||
<polygon points="6 69.09 0 68.82 6 68.49 6 69.09"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-2" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<g>
|
||||
<polygon points="66.29 50.01 66.57 44.01 70.97 43.99 71.29 49.99 66.29 50.01"/>
|
||||
<polygon points="67.16 31.34 67.75 18.67 69.62 18.66 70.3 31.33 67.16 31.34"/>
|
||||
<polygon points="68.34 6 68.62 0 68.94 6 68.34 6"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-3" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<g>
|
||||
<polygon points="87.43 66.29 93.43 66.57 93.44 70.97 87.44 71.29 87.43 66.29"/>
|
||||
<polygon points="106.1 67.16 118.76 67.75 118.77 69.62 106.11 70.3 106.1 67.16"/>
|
||||
<polygon points="131.43 68.34 137.43 68.62 131.44 68.94 131.43 68.34"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-4" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<g>
|
||||
<polygon points="71.14 87.43 70.86 93.43 66.46 93.44 66.14 87.44 71.14 87.43"/>
|
||||
<polygon points="70.27 106.1 69.68 118.76 67.82 118.77 67.14 106.11 70.27 106.1"/>
|
||||
<polygon points="69.09 131.43 68.82 137.43 68.49 131.44 69.09 131.43"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M69.26,70.79c1.93,0,1.93-3,0-3s-1.93,3,0,3h0Z"/>
|
||||
<g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-5" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path d="M80.01,134.2c-.42-4.29-.71-8.58-1.01-12.86.01-6.42,0-19.31,0-25.73,0,0,0-3.22,0-3.22v-1.61c-.03-2.95,1.19-5.88,3.31-7.94,1.69-1.64,3.92-2.74,6.27-3.02,2.05-.21,4.54-.04,6.55-.09,0,0,12.86,0,12.86,0h12.86c4.29.34,8.58.59,12.86,1-4.29.41-8.58.66-12.86,1-6.42,0-19.31,0-25.73,0-1.77.05-4.64-.1-6.3.08-1.92.23-3.73,1.12-5.12,2.47-1.74,1.7-2.72,4.08-2.7,6.51,0,0,0,1.61,0,1.61,0,0,0,3.22,0,3.22,0,6.41,0,19.32,0,25.73-.29,4.29-.59,8.58-1.01,12.86h0Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-6" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path d="M4.26,79.75c4.29-.42,8.58-.71,12.86-1.01,6.42.01,19.31,0,25.73,0,0,0,3.22,0,3.22,0h1.61c2.95-.03,5.88,1.19,7.94,3.31,1.64,1.69,2.74,3.92,3.02,6.27.21,2.05.04,4.54.09,6.55,0,0,0,12.86,0,12.86v12.86c-.34,4.29-.59,8.58-1,12.86-.41-4.29-.66-8.58-1-12.86,0-6.42,0-19.31,0-25.73-.05-1.77.1-4.64-.08-6.3-.23-1.92-1.12-3.73-2.47-5.12-1.7-1.74-4.08-2.72-6.51-2.7,0,0-1.61,0-1.61,0,0,0-3.22,0-3.22,0-6.41,0-19.32,0-25.73,0-4.29-.29-8.58-.59-12.86-1.01h0Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-7" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path d="M58.72,4c.42,4.29.71,8.58,1.01,12.86-.01,6.42,0,19.31,0,25.73,0,0,0,3.22,0,3.22v1.61c.03,2.95-1.19,5.88-3.31,7.94-1.69,1.64-3.92,2.74-6.27,3.02-2.05.21-4.54.04-6.55.09,0,0-12.86,0-12.86,0h-12.86c-4.29-.34-8.58-.59-12.86-1,4.29-.41,8.58-.66,12.86-1,6.42,0,19.31,0,25.73,0,1.77-.05,4.64.1,6.3-.08,1.92-.23,3.73-1.12,5.12-2.47,1.74-1.7,2.72-4.08,2.7-6.51,0,0,0-1.61,0-1.61,0,0,0-3.22,0-3.22,0-6.41,0-19.32,0-25.73.29-4.29.59-8.58,1.01-12.86h0Z"/>
|
||||
</g>
|
||||
<g id="_x3C_Радиальное_повторение_x3E_-8" data-name="_x3C_Радиальное_повторение_x3E_">
|
||||
<path d="M134.46,58.46c-4.29.42-8.58.71-12.86,1.01-6.42-.01-19.31,0-25.73,0,0,0-3.22,0-3.22,0h-1.61c-2.95.03-5.88-1.19-7.94-3.31-1.64-1.69-2.74-3.92-3.02-6.27-.21-2.05-.04-4.54-.09-6.55,0,0,0-12.86,0-12.86v-12.86c.34-4.29.59-8.58,1-12.86.41,4.29.66,8.58,1,12.86,0,6.42,0,19.31,0,25.73.05,1.77-.1,4.64.08,6.3.23,1.92,1.12,3.73,2.47,5.12,1.7,1.74,4.08,2.72,6.51,2.7,0,0,1.61,0,1.61,0,0,0,3.22,0,3.22,0,6.41,0,19.32,0,25.73,0,4.29.29,8.58.59,12.86,1.01h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.5 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.83 62.87">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_10" data-name="Слой_10">
|
||||
<g>
|
||||
<path class="cls-1" d="M41.75,62.87V4.78l-3.87,2.32-6.48,3.89c-9.59,5.76-17.51,13.94-22.94,23.72l-6.71,12.07"/>
|
||||
<path d="M41.56,62.87c0-16.54.05-34.31-.59-50.83-.1-2.42-.17-4.84-1.23-7.26,0,0,3.03,1.71,3.03,1.71-3.66,2.26-9.09,5.36-12.6,7.63-7.99,5.26-14.73,12.41-19.49,20.71,0,0-7.19,12.91-7.19,12.91,0,0-3.5-1.94-3.5-1.94,0,0,7.22-12.97,7.22-12.97,5.07-8.83,12.25-16.45,20.75-22.05,5.53-3.51,12.26-7.38,17.85-10.79,0,0-2.08,4.78-2.08,4.78-1.8,4.72-1.15,9.64-1.49,14.52-.19,8.41-.24,20.51-.33,29.04-.02,4.84-.03,9.68.01,14.52h-.37Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 859 B |
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.83 65.94">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1, .cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
stroke-width: .5px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_10" data-name="Слой_10">
|
||||
<g>
|
||||
<path class="cls-1" d="M41.75,62.87V4.78l-3.87,2.32-6.48,3.89c-9.59,5.76-17.51,13.94-22.94,23.72l-6.71,12.07"/>
|
||||
<path d="M41.56,62.87c0-16.54.05-34.31-.59-50.83-.1-2.42-.17-4.84-1.23-7.26,0,0,3.03,1.71,3.03,1.71-3.66,2.26-9.09,5.36-12.6,7.63-7.99,5.26-14.73,12.41-19.49,20.71,0,0-7.19,12.91-7.19,12.91,0,0-3.5-1.94-3.5-1.94,0,0,7.22-12.97,7.22-12.97,5.07-8.83,12.25-16.45,20.75-22.05,5.53-3.51,12.26-7.38,17.85-10.79,0,0-2.08,4.78-2.08,4.78-1.8,4.72-1.15,9.64-1.49,14.52-.19,8.41-.24,20.51-.33,29.04-.02,4.84-.03,9.68.01,14.52h-.37Z"/>
|
||||
</g>
|
||||
<circle class="cls-2" cx="41.47" cy="62.28" r="1"/>
|
||||
<path class="cls-2" d="M41.47,63.28v1.8h0c-.51.74-1.58.81-2.18.14l-.27-.31"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50.57 51.98">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1, .cls-2 {
|
||||
fill: #fff;
|
||||
stroke: #000;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_Слой_12" data-name="Слой_12">
|
||||
<polyline class="cls-2" points="21.49 2.23 21.49 22.58 21.79 51.97"/>
|
||||
<line class="cls-2" x1=".79" y1="41.97" x2="50.57" y2="41.97"/>
|
||||
<polygon class="cls-1" points="25.68 2.23 25.68 38.03 48.81 38.03 25.68 2.23"/>
|
||||
<polygon class="cls-1" points="17.7 2.23 17.7 38.03 .79 38.03 17.7 2.23"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 711 B |