Major architecture update + testink giteawebhook

This commit is contained in:
2025-10-08 11:45:22 +03:00
parent a607133032
commit 939f069681
18 changed files with 2252 additions and 186 deletions
@@ -4,6 +4,8 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.text.Editable;
import android.text.TextWatcher;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
@@ -27,6 +29,9 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
private Runnable tickerRunnable;
private android.widget.TextView textEmptyState;
private android.widget.TextView textTargetCount;
private android.widget.EditText editSearch;
private final java.util.List<AISVesselEntity> fullList = new java.util.ArrayList<>();
private String currentQuery = "";
// Данные нашего корабля
private double ourLatitude = 0;
@@ -46,10 +51,25 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
recyclerView = findViewById(R.id.recycler_ais_targets);
textEmptyState = findViewById(R.id.text_empty_state);
textTargetCount = findViewById(R.id.text_target_count);
editSearch = findViewById(R.id.edit_search);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new AisTargetsAdapter(new ArrayList<>(), this);
recyclerView.setAdapter(adapter);
editSearch.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
currentQuery = s != null ? s.toString().trim() : "";
applyFilterAndUpdate();
}
@Override
public void afterTextChanged(Editable s) { }
});
repository.observeAllAIS().observe(this, new Observer<List<AISVesselEntity>>() {
@Override
public void onChanged(List<AISVesselEntity> entities) {
@@ -57,23 +77,12 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
if (entities != null) {
java.util.Collections.sort(entities, (a, b) -> a.mmsi.compareTo(b.mmsi));
}
adapter.submitList(entities);
fullList.clear();
if (entities != null) fullList.addAll(entities);
applyFilterAndUpdate();
// Обновляем данные нашего корабля в адаптере
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);
}
}
});
@@ -96,6 +105,43 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
tickerHandler.postDelayed(tickerRunnable, 1000);
}
private void applyFilterAndUpdate() {
java.util.List<AISVesselEntity> filtered;
if (currentQuery == null || currentQuery.isEmpty()) {
filtered = new java.util.ArrayList<>(fullList);
} else {
String q = currentQuery.toLowerCase(java.util.Locale.ROOT);
filtered = new java.util.ArrayList<>();
for (AISVesselEntity e : fullList) {
if (matchesQuery(e, q)) {
filtered.add(e);
}
}
}
adapter.submitList(filtered);
int targetCount = filtered.size();
textTargetCount.setText("AIS цели: " + targetCount);
if (targetCount == 0) {
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);
}
}
private boolean matchesQuery(AISVesselEntity e, String q) {
if (e == null) return false;
// MMSI (строго строкой), имя, позывной, пункт назначения, IMO, тип судна
if (e.mmsi != null && e.mmsi.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
if (e.vesselName != null && e.vesselName.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
if (e.callSign != null && e.callSign.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
if (e.destination != null && e.destination.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
if (e.vesselType != null && e.vesselType.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
if (String.valueOf(e.imo).contains(q)) return true;
return false;
}
private void loadOurVesselData() {
repository.getLatestOwnVesselAsync(new Repository.RepositoryCallback<VesselEntity>() {
@Override
@@ -77,6 +77,7 @@ public class MainActivity extends AppCompatActivity {
private ImageButton btnCenterOnVessel;
private ImageButton btnMapOrientation;
private ImageButton btnCursorToggle;
private ImageButton btnSettings;
private ImageButton btnAisTargets;
private LinearLayout controlPanel;
@@ -88,6 +89,7 @@ public class MainActivity extends AppCompatActivity {
private Runnable compassUpdateRunnable;
private Runnable coordinatesUpdateRunnable;
private Runnable compassButtonRotationRunnable;
private static final long COMPASS_ANIM_DURATION_MS = 150;
private Vessel lastCompassVessel;
private Vessel lastCoordinatesVessel;
private static final long UI_UPDATE_THROTTLE_MS = 200; // 5 FPS максимум
@@ -160,6 +162,7 @@ public class MainActivity extends AppCompatActivity {
mapView = findViewById(R.id.map_view);
btnCenterOnVessel = findViewById(R.id.btn_center_vessel);
btnMapOrientation = findViewById(R.id.btn_map_orientation);
btnCursorToggle = findViewById(R.id.btn_cursor_toggle);
btnSettings = findViewById(R.id.btn_settings);
btnAisTargets = findViewById(R.id.btn_ais_targets);
controlPanel = findViewById(R.id.control_panel);
@@ -178,18 +181,36 @@ public class MainActivity extends AppCompatActivity {
coordinatesWidget.updateVessel(lastCoordinatesVessel);
}
};
// Периодическое обновление поворота кнопки компаса по bearing карты
// Периодическое обновление поворота кнопок (компас и ownship)
compassButtonRotationRunnable = () -> {
try {
if (btnMapOrientation != null && mapController.getCurrentMapInterface() != null) {
float bearing = mapController.getCurrentMapInterface().getBearing();
// Иконка должна указывать север: вращаем противоположно bearing карты
btnMapOrientation.setRotation(-bearing);
MapInterface mapIf = mapController != null ? mapController.getCurrentMapInterface() : null;
if (mapIf != null) {
float mapBearing = 0f;
try {
mapBearing = mapIf.getBearing();
} catch (Exception ignore) {}
// Кнопка компаса должна указывать на север (противоположно bearing карты)
float compassTarget = -mapBearing;
applySmoothRotation(btnMapOrientation, compassTarget);
// Кнопка корабля указывает курс относительно карты: course - mapBearing
if (btnCenterOnVessel != null && appCoordinator != null) {
Vessel own = appCoordinator.getOwnVessel();
if (own != null) {
double course = own.getCourse();
if (!Double.isNaN(course)) {
float shipTarget = (float)(course - mapBearing);
applySmoothRotation(btnCenterOnVessel, shipTarget);
}
}
}
}
} catch (Exception ignore) {}
// Планируем следующее обновление
if (uiThrottleHandler != null) {
uiThrottleHandler.postDelayed(compassButtonRotationRunnable, UI_UPDATE_THROTTLE_MS);
uiThrottleHandler.postDelayed(compassButtonRotationRunnable, Math.max(100, UI_UPDATE_THROTTLE_MS));
}
};
tvGpsAge = findViewById(R.id.tv_gps_age);
@@ -211,6 +232,8 @@ public class MainActivity extends AppCompatActivity {
private void setupButtonListeners() {
if (btnCenterOnVessel != null) btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
if (btnMapOrientation != null) btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
if (btnCursorToggle != null) btnCursorToggle.setOnClickListener(v -> toggleCursor());
if (btnCursorToggle != null) btnCursorToggle.setOnLongClickListener(v -> { toggleCursor(); return true; });
if (btnSettings != null) btnSettings.setOnClickListener(v -> showSettings());
if (btnAisTargets != null) btnAisTargets.setOnClickListener(v -> openAisTargets());
@@ -297,13 +320,32 @@ public class MainActivity extends AppCompatActivity {
compassView.post(() -> {
updateControlPanelPosition();
});
// Стартуем обновление поворота кнопки компаса
if (uiThrottleHandler != null) {
// Стартуем цикл обновления поворотов кнопок
startCompassButtonsLoop();
}
private void startCompassButtonsLoop() {
if (uiThrottleHandler != null && compassButtonRotationRunnable != null) {
uiThrottleHandler.removeCallbacks(compassButtonRotationRunnable);
uiThrottleHandler.post(compassButtonRotationRunnable);
}
}
private void applySmoothRotation(ImageButton button, float targetRotation) {
if (button == null) return;
try {
float current = button.getRotation();
float normalizedTarget = targetRotation % 360f;
if (normalizedTarget < -180f) normalizedTarget += 360f;
if (normalizedTarget > 180f) normalizedTarget -= 360f;
float delta = normalizedTarget - current;
if (delta > 180f) delta -= 360f;
if (delta < -180f) delta += 360f;
float finalTarget = current + delta;
button.animate().rotation(finalTarget).setDuration(COMPASS_ANIM_DURATION_MS).setInterpolator(new android.view.animation.DecelerateInterpolator()).start();
} catch (Exception ignore) {}
}
private void setupCoordinatesWidget() {
// Настраиваем слушатель изменения размера dock-виджета
coordinatesWidget.setOnDockResizeListener(newHeight -> {
@@ -322,30 +364,11 @@ public class MainActivity extends AppCompatActivity {
updateControlPanelPosition();
});
// Устанавливаем виджет координат в dock-режим внизу экрана
// Устанавливаем виджет координат в dock-режим внизу экрана без тестовых данных
coordinatesWidget.post(() -> {
Log.d(TAG, "Setting coordinates widget to dock mode");
coordinatesWidget.setDocked(true, false, 0, 0); // false = dock снизу
coordinatesWidget.invalidate(); // Принудительная отрисовка
// Принудительно обновляем виджет с тестовыми данными (в фоне)
android.os.Handler bgHandler = new android.os.Handler(android.os.Looper.getMainLooper());
bgHandler.post(() -> {
try {
Vessel testVessel = new Vessel();
testVessel.setLatitude(55.7558);
testVessel.setLongitude(37.6176);
testVessel.setSpeed(5.5);
testVessel.setCourse(45.0);
testVessel.setAccuracy(3.0f);
coordinatesWidget.updateVessel(testVessel);
// Используем throttled версию
updateControlPanelPositionThrottled();
} catch (Exception e) {
Log.e(TAG, "Ошибка при инициализации тестового виджета: " + e.getMessage(), e);
}
});
});
}
@@ -905,6 +928,31 @@ public class MainActivity extends AppCompatActivity {
startActivity(intent);
}
/**
* Переключает отображение курсора на карте и сохраняет состояние
*/
private void toggleCursor() {
if (mapController == null || mapController.getCurrentMapInterface() == null || settingsManager == null) return;
boolean enabled = settingsManager.isCursorEnabled();
boolean newState = !enabled;
settingsManager.setCursorEnabled(newState);
try {
if (newState) {
mapController.getCurrentMapInterface().showCursor();
mapController.getCurrentMapInterface().updateCursorFromMapCenter();
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapController.getCurrentMapInterface()).forceCheckAisVesselUnderCursor();
}
Toast.makeText(this, "Курсор включён", Toast.LENGTH_SHORT).show();
} else {
mapController.getCurrentMapInterface().hideCursor();
Toast.makeText(this, "Курсор выключен", Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
Log.w(TAG, "toggleCursor error: " + e.getMessage());
}
}
/**
* Обновляет позицию панели управления с throttling
*/
@@ -1081,23 +1129,26 @@ public class MainActivity extends AppCompatActivity {
// Обрабатываем возможный интент центрирования
handleCenterIntentIfAny(getIntent());
// Восстанавливаем курсор после возврата в активность
// Восстанавливаем курсор после возврата в активность согласно настройке
if (mapController.getCurrentMapInterface() != null) {
boolean cursorEnabled = settingsManager.isCursorEnabled();
if (cursorEnabled) {
mapController.getCurrentMapInterface().showCursor();
// Обновляем координаты курсора с центра карты
mapController.getCurrentMapInterface().updateCursorFromMapCenter();
// Принудительно проверяем AIS судно под курсором для восстановления панели
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapController.getCurrentMapInterface()).forceCheckAisVesselUnderCursor();
}
} else {
// Явно скрываем при возвращении, если выключен в настройках
mapController.getCurrentMapInterface().hideCursor();
}
}
// Проверяем разрешения и запускаем контроллеры
checkPermissions();
// Перезапускаем цикл поворота кнопок после возврата в активити
startCompassButtonsLoop();
}
@Override
@@ -1148,6 +1199,10 @@ public class MainActivity extends AppCompatActivity {
// Очищаем информацию о AIS судне при паузе активности
if (mapController.getCurrentMapInterface() != null) {
mapController.getCurrentMapInterface().clearAisVesselInfo();
// Принудительно удаляем overlay курсора, чтобы не накапливался между активити
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapController.getCurrentMapInterface()).removeCursorOverlay();
}
}
}
@@ -1324,6 +1379,11 @@ public class MainActivity extends AppCompatActivity {
// Применяем настройки курсора
applyCursorSettings(cursorEnabled);
// Применяем дебаг-режим на карте (красный квадрат)
boolean debugEnabled = data.getBooleanExtra("debug_enabled", settingsManager.isDebugEnabled());
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
((MapLibreMapImpl) mapController.getCurrentMapInterface()).setDebugMode(debugEnabled);
}
if (needsRestart) {
Log.i(TAG, "Требуется перезапуск сервисов");
@@ -38,7 +38,7 @@ public class SettingsActivity extends AppCompatActivity {
private SwitchMaterial switchVibrationEnabled;
private SwitchMaterial switchSoundEnabled;
private SwitchMaterial switchKeepScreenOn;
private SwitchMaterial switchCursorEnabled;
private SwitchMaterial switchDebugEnabled;
private Button btnCancel;
private Button btnSave;
private Button btnClearPath;
@@ -104,7 +104,7 @@ public class SettingsActivity extends AppCompatActivity {
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);
switchDebugEnabled = findViewById(R.id.switch_debug_enabled);
btnCancel = findViewById(R.id.btn_cancel);
btnSave = findViewById(R.id.btn_save);
btnClearPath = findViewById(R.id.btn_clear_path);
@@ -154,8 +154,8 @@ public class SettingsActivity extends AppCompatActivity {
// Настройки экрана
switchKeepScreenOn.setChecked(settingsManager.isKeepScreenOnEnabled());
// Настройки курсора
switchCursorEnabled.setChecked(settingsManager.isCursorEnabled());
// Дебаг
switchDebugEnabled.setChecked(settingsManager.isDebugEnabled());
// Путь и предсказание
etPathMaxPoints.setText(String.valueOf(settingsManager.getPathMaxPoints()));
@@ -182,7 +182,6 @@ public class SettingsActivity extends AppCompatActivity {
originalVibrationEnabled = settingsManager.isVibrationEnabled();
originalSoundEnabled = settingsManager.isSoundEnabled();
originalKeepScreenOnEnabled = settingsManager.isKeepScreenOnEnabled();
originalCursorEnabled = settingsManager.isCursorEnabled();
Log.i(TAG, "Оригинальные настройки сохранены");
}
@@ -314,7 +313,8 @@ public class SettingsActivity extends AppCompatActivity {
settingsManager.setVibrationEnabled(switchVibrationEnabled.isChecked());
settingsManager.setSoundEnabled(switchSoundEnabled.isChecked());
settingsManager.setKeepScreenOnEnabled(switchKeepScreenOn.isChecked());
settingsManager.setCursorEnabled(switchCursorEnabled.isChecked());
boolean debugEnabled = switchDebugEnabled.isChecked();
settingsManager.setDebugEnabled(debugEnabled);
// Путь и предсказание
try { settingsManager.setPathMaxPoints(Integer.parseInt(etPathMaxPoints.getText().toString().trim())); } catch (Exception ignored) {}
@@ -338,7 +338,8 @@ 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());
resultIntent.putExtra("cursor_enabled", settingsManager.isCursorEnabled());
resultIntent.putExtra("debug_enabled", debugEnabled);
setResult(RESULT_OK, resultIntent);
@@ -13,6 +13,7 @@ import com.grigowashere.aismap.utils.SettingsManager;
import java.util.List;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -42,7 +43,7 @@ public class AppCoordinator implements
// Состояние приложения
private Vessel ownVessel;
private List<AISVessel> aisVessels;
private Map<String, AISVessel> aisVessels;
private Map<String, VesselPathController> aisPathControllers;
private SettingsManager settingsManager;
private VesselPathController pathController;
@@ -72,7 +73,7 @@ public class AppCoordinator implements
public AppCoordinator(Context context) {
this.context = context;
this.ownVessel = new Vessel();
this.aisVessels = new ArrayList<>();
this.aisVessels = new LinkedHashMap<>();
this.aisPathControllers = new HashMap<>();
this.settingsManager = new SettingsManager(context);
this.pathController = new VesselPathController(context, settingsManager);
@@ -268,20 +269,16 @@ public class AppCoordinator implements
}
}
// Обновляем дополнительные данные
if (vessel.getCourse() > 0) {
ownVessel.setCourse(vessel.getCourse());
updateCompass();
}
if (vessel.getSpeed() > 0) {
ownVessel.setSpeed(vessel.getSpeed());
}
if (vessel.getSatellites() > 0) {
ownVessel.setSatellites(vessel.getSatellites());
}
if (vessel.getAltitude() != 0) {
ownVessel.setAltitude(vessel.getAltitude());
}
// Обновляем дополнительные данные (без фильтров по нулю, чтобы корректно обрабатывать сбросы)
ownVessel.setCourse(vessel.getCourse());
ownVessel.setSpeed(vessel.getSpeed());
ownVessel.setSatellites(vessel.getSatellites());
ownVessel.setActiveSatellites(vessel.getActiveSatellites());
ownVessel.setAltitude(vessel.getAltitude());
ownVessel.setFixQuality(vessel.getFixQuality());
// Обновляем компас после изменения курса
updateCompass();
// Сохраняем в БД
dataController.saveVesselPosition(ownVessel);
@@ -343,8 +340,10 @@ public class AppCoordinator implements
}
} else {
// Добавляем новое судно
synchronized (aisVessels) {
aisVessels.add(vessel);
if (vessel.getMmsi() != null) {
synchronized (aisVessels) {
aisVessels.put(vessel.getMmsi(), vessel);
}
}
// Если это новое судно сразу пришло с safety-сообщением — уведомим
@@ -404,7 +403,11 @@ public class AppCoordinator implements
if (aisVessels != null) {
synchronized (this.aisVessels) {
this.aisVessels.clear();
this.aisVessels.addAll(aisVessels);
for (AISVessel ais : aisVessels) {
if (ais.getMmsi() != null) {
this.aisVessels.put(ais.getMmsi(), ais);
}
}
}
}
@@ -510,7 +513,7 @@ public class AppCoordinator implements
if (aisVessels != null && !aisVessels.isEmpty()) {
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
synchronized (aisVessels) {
for (AISVessel vessel : aisVessels) {
for (AISVessel vessel : aisVessels.values()) {
uiDataNotifier.onAISVesselChanged(vessel);
}
}
@@ -522,21 +525,19 @@ public class AppCoordinator implements
// Вспомогательные методы
private AISVessel findAISVesselByMMSI(String mmsi) {
synchronized (aisVessels) {
for (AISVessel vessel : aisVessels) {
if (mmsi.equals(vessel.getMmsi())) {
return vessel;
}
}
if (mmsi == null) {
return null;
}
synchronized (aisVessels) {
return aisVessels.get(mmsi);
}
return null;
}
private List<AISVessel> getNearbyVessels() {
List<AISVessel> nearby = new ArrayList<>();
double maxDistance = 10000; // 10 км в метрах
for (AISVessel vessel : aisVessels) {
for (AISVessel vessel : aisVessels.values()) {
double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel);
if (distance <= maxDistance) {
nearby.add(vessel);
@@ -631,7 +632,7 @@ public class AppCoordinator implements
public List<AISVessel> getAISVessels() {
synchronized (aisVessels) {
return new ArrayList<>(aisVessels);
return new ArrayList<>(aisVessels.values());
}
}
@@ -7,6 +7,11 @@ import com.grigowashere.aismap.utils.LogSender;
import java.util.List;
import java.util.ArrayList;
import java.time.Clock;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
/**
* Контроллер для парсинга NMEA сообщений
@@ -174,11 +179,98 @@ public class NMEAParser {
*/
private String getField(String[] fields, int index) {
if (index < fields.length && !fields[index].trim().isEmpty()) {
return fields[index].trim();
return sanitizeField(fields[index].trim());
}
return null;
}
/**
* Удаляет суффикс контрольной суммы (*xx) из значения поля
*/
private String sanitizeField(String value) {
if (value == null) return null;
int starPos = value.indexOf('*');
if (starPos >= 0) {
return value.substring(0, starPos);
}
return value;
}
/**
* Конвертирует строку UTC времени hhmmss[.ss] в epoch millis для текущей UTC даты
*/
private long utcTimeToEpochMillis(String utcTimeStr) {
if (utcTimeStr == null || utcTimeStr.length() < 6) return 0L;
try {
String hh = utcTimeStr.substring(0, 2);
String mm = utcTimeStr.substring(2, 4);
String ss = utcTimeStr.substring(4); // может содержать дробную часть
int hours = Integer.parseInt(hh);
int minutes = Integer.parseInt(mm);
int wholeSeconds;
int nanoAdjustment;
if (ss.contains(".")) {
String[] parts = ss.split("\\.", 2);
wholeSeconds = Integer.parseInt(parts[0]);
// дробную часть переводим в наносекунды (до 9 знаков)
String frac = parts[1];
if (frac.length() > 9) frac = frac.substring(0, 9);
// правым дополнением до 9 знаков
while (frac.length() < 9) frac += "0";
nanoAdjustment = Integer.parseInt(frac);
} else {
wholeSeconds = Integer.parseInt(ss);
nanoAdjustment = 0;
}
LocalTime time = LocalTime.of(hours, minutes, wholeSeconds, nanoAdjustment);
// Без даты epoch возвращать нельзя — оставляем 0, чтобы не писать некорректные значения
return 0L;
} catch (Exception e) {
return 0L;
}
}
/**
* Конвертирует дату (DDMMYY) и время (HHMMSS[.SS]) UTC в epoch millis
*/
private long utcDateTimeToEpochMillis(String dateDDMMYY, String timeHHMMSS) {
if (dateDDMMYY == null || dateDDMMYY.length() != 6 || timeHHMMSS == null || timeHHMMSS.length() < 6) return 0L;
try {
int day = Integer.parseInt(dateDDMMYY.substring(0, 2));
int month = Integer.parseInt(dateDDMMYY.substring(2, 4));
int year = 2000 + Integer.parseInt(dateDDMMYY.substring(4, 6));
String hh = timeHHMMSS.substring(0, 2);
String mm = timeHHMMSS.substring(2, 4);
String ss = timeHHMMSS.substring(4);
int hours = Integer.parseInt(hh);
int minutes = Integer.parseInt(mm);
int wholeSeconds;
int nanoAdjustment;
if (ss.contains(".")) {
String[] parts = ss.split("\\.", 2);
wholeSeconds = Integer.parseInt(parts[0]);
String frac = parts[1];
if (frac.length() > 9) frac = frac.substring(0, 9);
while (frac.length() < 9) frac += "0";
nanoAdjustment = Integer.parseInt(frac);
} else {
wholeSeconds = Integer.parseInt(ss);
nanoAdjustment = 0;
}
LocalDate date = LocalDate.of(year, month, day);
LocalTime time = LocalTime.of(hours, minutes, wholeSeconds, nanoAdjustment);
ZonedDateTime zdt = ZonedDateTime.of(date, time, ZoneOffset.UTC);
return zdt.toInstant().toEpochMilli();
} catch (Exception e) {
return 0L;
}
}
/**
* Безопасно парсит double значение из поля
*/
@@ -311,7 +403,7 @@ public class NMEAParser {
}
}
ownVessel.setSatellites(satellites);
ownVessel.setActiveSatellites(satellites);
ownVessel.setAltitude(altitude);
// Синхронизируем с GPSLocationListener для получения активных спутников
@@ -343,6 +435,16 @@ public class NMEAParser {
// Поле 8: курс в градусах
double course = parseDoubleField(fields, 8, 0.0);
// Устанавливаем время фикса только если есть и дата, и время
String timeStr = getField(fields, 1); // HHMMSS.SS
String dateStr = getField(fields, 9); // DDMMYY
if (timeStr != null && timeStr.length() >= 6) {
ownVessel.setFixTimeNmea(timeStr);
}
long epochMillis = utcDateTimeToEpochMillis(dateStr, timeStr);
if (epochMillis > 0) {
ownVessel.setFixTime(epochMillis);
}
// Убираем шумный лог RMC данных
// В гибридном режиме не обновляем координаты
@@ -414,30 +516,56 @@ public class NMEAParser {
* Формат: $GPGLL,lat,N/S,lon,E/W,time,status,mode*checksum
*/
private void parseGLL(String[] fields) {
if (hybridMode) {
// Убираем шумный лог игнорирования GLL
return;
// Разбираем время фикса (поле 5), статус (поле 6) и режим (поле 7)
String utcTimeStr = getField(fields, 5); // hhmmss.ss
String status = getField(fields, 6); // A/V
String mode = getField(fields, 7); // A/D/E/M/S/N (может отсутствовать)
// Устанавливаем fixQuality на основе статуса и режима
if (status != null) {
if ("A".equals(status)) {
// Валидные данные: уточняем по mode
if (mode != null) {
switch (mode) {
case "A": ownVessel.setFixQuality("AUTONOMOUS"); break;
case "D": ownVessel.setFixQuality("DIFFERENTIAL"); break;
case "E": ownVessel.setFixQuality("ESTIMATED"); break;
case "M": ownVessel.setFixQuality("MANUAL"); break;
case "S": ownVessel.setFixQuality("SIMULATOR"); break;
case "N": ownVessel.setFixQuality("NOT_VALID"); break;
default: ownVessel.setFixQuality("AUTONOMOUS"); break;
}
} else {
ownVessel.setFixQuality("AUTONOMOUS");
}
} else {
ownVessel.setFixQuality("NOT_VALID");
}
}
// Убираем шумный лог парсинга GLL
// Поля 1,2: широта и направление
String latStr = getField(fields, 1);
String latDir = getField(fields, 2);
if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, latDir.equals("N"));
ownVessel.setLatitude(latitude);
// GLL не содержит дату — epoch не пишем, но строковое время сохраним
if (utcTimeStr != null && utcTimeStr.length() >= 6) {
ownVessel.setFixTimeNmea(utcTimeStr);
}
// Поля 3,4: долгота и направление
String lonStr = getField(fields, 3);
String lonDir = getField(fields, 4);
if (lonStr != null && lonDir != null) {
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
ownVessel.setLongitude(longitude);
}
// Если не в гибридном режиме — обновляем координаты
if (!hybridMode) {
// Поля 1,2: широта и направление
String latStr = getField(fields, 1);
String latDir = getField(fields, 2);
if (latStr != null && latDir != null) {
double latitude = parseCoordinate(latStr, "N".equals(latDir));
ownVessel.setLatitude(latitude);
}
// Убираем шумный лог GLL координат
// Поля 3,4: долгота и направление
String lonStr = getField(fields, 3);
String lonDir = getField(fields, 4);
if (lonStr != null && lonDir != null) {
double longitude = parseCoordinate(lonStr, "E".equals(lonDir));
ownVessel.setLongitude(longitude);
}
}
if (listener != null) {
listener.onVesselUpdated(ownVessel);
@@ -470,7 +598,7 @@ public class NMEAParser {
}
// Убираем шумный лог GSV спутников
int totalSatellites1 = 0;
// Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
if (i + 3 < fields.length) {
@@ -478,7 +606,7 @@ public class NMEAParser {
String elevation = getField(fields, i + 1);
String azimuth = getField(fields, i + 2);
String snr = getField(fields, i + 3);
totalSatellites1++;
if (satId != null) {
Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
satId, elevation, azimuth, snr));
@@ -507,7 +635,7 @@ public class NMEAParser {
// Обновляем общее количество спутников
int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites;
ownVessel.setSatellites(totalSatellites);
ownVessel.setSatellites(totalSatellites1);
// Синхронизируем с GPSLocationListener для получения активных спутников
if (gpsLocationListener != null) {
@@ -557,7 +685,7 @@ public class NMEAParser {
}
}
ownVessel.setSatellites(satellites);
ownVessel.setActiveSatellites(satellites);
ownVessel.setAltitude(altitude);
// Синхронизируем с GPSLocationListener для получения активных спутников
@@ -592,6 +720,14 @@ public class NMEAParser {
Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d",
year, month, day, timeStr, timezoneHours, timezoneMinutes));
// Если и дата, и время валидны — выставим epoch fixTime
if (year > 0 && month > 0 && day > 0 && timeStr != null && timeStr.length() >= 6) {
String dateStr = String.format(java.util.Locale.US, "%02d%02d%02d", day, month, (year % 100));
long epochMillis = utcDateTimeToEpochMillis(dateStr, timeStr);
if (epochMillis > 0) {
ownVessel.setFixTime(epochMillis);
}
}
// Обновляем время последнего обновления
ownVessel.setLastUpdate(java.time.LocalDateTime.now());
@@ -613,6 +749,17 @@ public class NMEAParser {
private void parseGSA(String[] fields) {
// Убираем шумный лог парсинга GSA
// Mode1 (авто/ручной) и тип фикса (1/2/3)
String mode1 = getField(fields, 1); // A/M
int fixType = parseIntField(fields, 2, 1); // 1=no fix, 2=2D, 3=3D
String fixQuality = "NO_FIX";
if (fixType == 2) {
fixQuality = ("A".equals(mode1) ? "AUTO_2D" : ("M".equals(mode1) ? "MANUAL_2D" : "2D"));
} else if (fixType == 3) {
fixQuality = ("A".equals(mode1) ? "AUTO_3D" : ("M".equals(mode1) ? "MANUAL_3D" : "3D"));
}
ownVessel.setFixQuality(fixQuality);
// Подсчитываем активные спутники (поля 3-14 содержат ID спутников)
int activeSatellites = 0;
for (int i = 3; i <= 14 && i < fields.length; i++) {
@@ -623,6 +770,7 @@ public class NMEAParser {
}
}
// Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
double pdop = 0.0;
double hdop = 0.0;
@@ -16,6 +16,8 @@ import com.grigowashere.aismap.controllers.AppCoordinator;
import com.grigowashere.aismap.view.CursorOverlay;
import com.grigowashere.aismap.R;
import android.view.ViewGroup;
import android.view.View;
import android.view.ViewParent;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -193,14 +195,39 @@ public class MapLibreMapImpl implements MapInterface {
// VesselPathController будет установлен через setVesselPathController()
this.pathController = null;
this.cursorOverlay = new CursorOverlay(context);
// Инициализируем флаг отладки из настроек
this.debugMode = settingsManager.isDebugEnabled();
// Добавляем overlay курсора в MapView
if (mapView instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) mapView;
// Проверяем, не добавлен ли уже курсор
if (parent.findViewById(R.id.cursor_cross) == null) {
parent.addView(cursorOverlay.getView());
// Добавляем overlay курсора НАД картой: в родителя MapView, чтобы он был поверх GL слоя
ViewParent mapParent = mapView.getParent();
if (mapParent instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) mapParent;
// Удаляем все прежние cursor_overlay из родителя (на случай повторной инициализации)
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
View child = parent.getChildAt(i);
Object tag = child.getTag();
if (tag != null && "cursor_overlay".equals(tag)) {
parent.removeViewAt(i);
}
}
View v = cursorOverlay.getView();
if (v.getParent() != parent) {
if (v.getParent() instanceof ViewGroup) {
((ViewGroup) v.getParent()).removeView(v);
}
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
);
// Вставляем поверх MapView
int mapIndex = parent.indexOfChild(mapView);
int insertIndex = Math.min(mapIndex + 1, parent.getChildCount());
parent.addView(v, insertIndex, lp);
}
// Поднимаем на передний план
v.bringToFront();
v.setTranslationZ(2f);
}
// Инициализируем throttling для карты
@@ -288,7 +315,19 @@ public class MapLibreMapImpl implements MapInterface {
if (maplibreMap != null) {
maplibreMap.removeOnMapClickListener(onMapClickListener);
}
// Удаляем overlay курсора из родителя, чтобы не накапливался между активити
if (mapView != null) {
ViewParent mapParent = mapView.getParent();
if (mapParent instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) mapParent;
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
View child = parent.getChildAt(i);
Object tag = child.getTag();
if (tag != null && "cursor_overlay".equals(tag)) {
parent.removeViewAt(i);
}
}
}
mapView.onStop();
}
staleHandler.removeCallbacks(staleRunnable);
@@ -1062,30 +1101,28 @@ public class MapLibreMapImpl implements MapInterface {
Log.d(TAG, "appendOwnPathPoint: skipping zero coordinates");
return;
}
// Проверяем, изменились ли координаты (строгая проверка на дублирование)
if (ownPathCoords.length() > 0) {
JSONArray lastPoint = ownPathCoords.getJSONArray(ownPathCoords.length() - 1);
double lastLon = lastPoint.getDouble(0);
double lastLat = lastPoint.getDouble(1);
// ВРЕМЕННО ОТКЛЮЧАЕМ ПРОВЕРКУ РАССТОЯНИЯ ДЛЯ ТЕСТИРОВАНИЯ
// Проверяем, изменились ли координаты (строгая проверка на дублирование)
// if (ownPathCoords.length() > 0) {
// JSONArray lastPoint = ownPathCoords.getJSONArray(ownPathCoords.length() - 1);
// double lastLon = lastPoint.getDouble(0);
// double lastLat = lastPoint.getDouble(1);
//
// // Строгая проверка на точное совпадение координат
// if (lon == lastLon && lat == lastLat) {
// Log.d(TAG, "appendOwnPathPoint: exact duplicate coordinates, skipping");
// return;
// }
//
// double distance = Math.sqrt(Math.pow(lon - lastLon, 2) + Math.pow(lat - lastLat, 2));
// Log.d(TAG, "appendOwnPathPoint: distance=" + distance + " (threshold=0.00001)");
// if (distance < 0.00001) { // примерно 1 метр
// Log.d(TAG, "appendOwnPathPoint: vessel not moving, distance=" + distance);
// return;
// }
// }
// Строгая проверка на точное совпадение координат
if (lon == lastLon && lat == lastLat) {
Log.d(TAG, "appendOwnPathPoint: exact duplicate coordinates, skipping");
return;
}
// ЗАХАРДКОДИМ МАКСИМАЛЬНОЕ КОЛИЧЕСТВО ТОЧЕК ДЛЯ ТЕСТИРОВАНИЯ
int max = 5000; // settingsManager.getPathMaxPoints();
double distance = Math.sqrt(Math.pow(lon - lastLon, 2) + Math.pow(lat - lastLat, 2));
Log.d(TAG, "appendOwnPathPoint: distance=" + distance + " (threshold=0.00001)");
if (distance < 0.00001) { // примерно 1 метр
Log.d(TAG, "appendOwnPathPoint: vessel not moving, distance=" + distance);
return;
}
}
// Лимит точек пути из настроек
int max = settingsManager.getPathMaxPoints();
if (ownPathCoords.length() >= max) {
// удаляем из начала
ownPathCoords.remove(0);
@@ -1940,15 +1977,42 @@ public class MapLibreMapImpl implements MapInterface {
@Override
public void showCursor() {
if (cursorOverlay != null) {
try {
if (cursorOverlay == null) {
cursorOverlay = new com.grigowashere.aismap.view.CursorOverlay(context);
}
// Убедимся, что overlay добавлен в иерархию и находится поверх карты
if (mapView instanceof android.view.ViewGroup) {
android.view.ViewGroup parent = (android.view.ViewGroup) mapView;
android.view.View v = cursorOverlay.getView();
if (v.getParent() != parent) {
if (v.getParent() instanceof android.view.ViewGroup) {
((android.view.ViewGroup) v.getParent()).removeView(v);
}
android.view.ViewGroup.LayoutParams lp = new android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT
);
parent.addView(v, lp);
}
v.bringToFront();
v.setTranslationZ(1f);
}
cursorOverlay.showCursor();
}
// Обновим размеры, координаты и попробуем восстановить панель AIS
updateScreenDimensions();
updateCursorFromMapCenter();
// Подпишемся на движение камеры (на случай если слушатель потерян)
setupMapMovementListener();
forceCheckAisVesselUnderCursor();
} catch (Exception ignore) {}
}
@Override
public void hideCursor() {
if (cursorOverlay != null) {
cursorOverlay.hideCursor();
clearAisVesselInfo();
}
}
@@ -1961,25 +2025,64 @@ public class MapLibreMapImpl implements MapInterface {
@Override
public void updateCursorFromMapCenter() {
if (cursorOverlay != null && maplibreMap != null && mapView != null) {
if (cursorOverlay != null && mapView != null) {
try {
// Получаем координаты центра карты
org.maplibre.android.geometry.LatLng center = maplibreMap.getCameraPosition().target;
// Получаем координаты центра карты (если карта уже готова)
org.maplibre.android.geometry.LatLng center = maplibreMap != null ? maplibreMap.getCameraPosition().target : null;
Log.d(TAG, String.format("updateCursorFromMapCenter: center=%.6f,%.6f",
center.getLatitude(), center.getLongitude()));
center != null ? center.getLatitude() : 0,
center != null ? center.getLongitude() : 0));
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
// Проверяем, есть ли AIS судно под курсором
checkAisVesselUnderCursor(center);
if (center != null) {
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
// Проверяем, есть ли AIS судно под курсором
checkAisVesselUnderCursor(center);
}
} catch (Exception e) {
Log.w(TAG, "updateCursorFromMapCenter: MapView may be destroyed: " + e.getMessage());
}
} else {
Log.d(TAG, "updateCursorFromMapCenter: cursorOverlay, maplibreMap или mapView равны null");
Log.d(TAG, "updateCursorFromMapCenter: cursorOverlay или mapView равны null");
}
}
/**
* Публичный метод для принудительного удаления cursor overlay из вью-иерархии
* Используется активностью в onPause/onStop, чтобы исключить наслоение при возврате
*/
public void removeCursorOverlay() {
try {
if (cursorOverlay != null) {
cursorOverlay.hideCursor();
}
if (mapView != null) {
// Удаляем из родителя MapView
ViewParent mapParent = mapView.getParent();
if (mapParent instanceof ViewGroup) {
ViewGroup parent = (ViewGroup) mapParent;
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
View child = parent.getChildAt(i);
Object tag = child.getTag();
if (tag != null && "cursor_overlay".equals(tag)) {
parent.removeViewAt(i);
}
}
}
// На всякий случай: уберём из самого MapView (если когда-то добавлялся туда)
if (mapView instanceof ViewGroup) {
ViewGroup mv = (ViewGroup) mapView;
for (int i = mv.getChildCount() - 1; i >= 0; i--) {
View child = mv.getChildAt(i);
Object tag = child.getTag();
if (tag != null && "cursor_overlay".equals(tag)) {
mv.removeViewAt(i);
}
}
}
}
} catch (Exception ignore) {}
}
/**
* Проверяет, есть ли AIS судно под курсором (в центре экрана)
*/
@@ -30,6 +30,7 @@ public class Vessel {
private float accuracy; // точность в метрах
private long fixTime; // время последнего фикса
private String fixQuality; // качество фикса (GPS, DGPS, RTK и т.д.)
private String fixTimeNmea; // время фикса как пришло в NMEA (строка HHMMSS[.SS])
public Vessel() {
this.lastUpdate = LocalDateTime.now();
@@ -104,6 +105,9 @@ public class Vessel {
public String getFixQuality() { return fixQuality; }
public void setFixQuality(String fixQuality) { this.fixQuality = fixQuality; }
public String getFixTimeNmea() { return fixTimeNmea; }
public void setFixTimeNmea(String fixTimeNmea) { this.fixTimeNmea = fixTimeNmea; }
/**
* Обновляет данные судна
*/
@@ -147,7 +147,22 @@ public class BottomSheetsManager {
if (tvGPSQuality != null) tvGPSQuality.setText((vessel.getGPSQualityDescription()!=null) ? String.format("📊 Качество GPS: %s", vessel.getGPSQualityDescription()) : "📊 Качество GPS: --");
if (tvSatellites != null) tvSatellites.setText((vessel.getSatellites()>0) ? String.format("Спутники: %d/%d", vessel.getActiveSatellites(), vessel.getSatellites()) : "Спутники: --/--");
if (tvDOP != null) tvDOP.setText((vessel.getPdop()>0) ? String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", vessel.getPdop(), vessel.getHdop(), vessel.getVdop()) : "📈 DOP: PDOP=-- HDOP=-- VDOP=--");
if (tvFixTime != null) tvFixTime.setText((vessel.getFixTime()>0) ? String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(new java.util.Date(vessel.getFixTime()))) : "🕐 Время фикса: --");
if (tvFixTime != null) {
String t = vessel.getFixTimeNmea();
if (t != null && !t.isEmpty()) {
String pretty = t;
int dot = pretty.indexOf('.');
if (dot > 0) pretty = pretty.substring(0, dot);
if (pretty.length() == 6) {
pretty = pretty.substring(0,2) + ":" + pretty.substring(2,4) + ":" + pretty.substring(4,6);
}
tvFixTime.setText(String.format("🕐 Время фикса: %s", pretty));
} else if (vessel.getFixTime() > 0) {
tvFixTime.setText(String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(new java.util.Date(vessel.getFixTime()))));
} else {
tvFixTime.setText("🕐 Время фикса: --");
}
}
if (tvFixQuality != null) tvFixQuality.setText((vessel.getFixQuality()!=null) ? String.format("🔒 Качество фикса: %s", vessel.getFixQuality()) : "🔒 Качество фикса: --");
}
@@ -33,6 +33,7 @@ public class SettingsManager {
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 String KEY_NOTIFICATIONS_ENABLED = "notifications_enabled";
private static final String KEY_DEBUG_ENABLED = "debug_enabled";
private static final String KEY_ANDROID_GPS_ENABLED = "android_gps_enabled";
// Значения по умолчанию
@@ -56,6 +57,7 @@ public class SettingsManager {
private static final boolean DEFAULT_CURSOR_ENABLED = false;
private static final boolean DEFAULT_NOTIFICATIONS_ENABLED = true;
private static final boolean DEFAULT_ANDROID_GPS_ENABLED = true;
private static final boolean DEFAULT_DEBUG_ENABLED = false;
// Режимы работы с данными
public static final String DATA_MODE_HYBRID = "hybrid";
@@ -469,4 +471,19 @@ public class SettingsManager {
Log.i(TAG, "Уведомления: " + (enabled ? "включены" : "выключены"));
}
/**
* Проверяет, включен ли дебаг-режим
*/
public boolean isDebugEnabled() {
return prefs.getBoolean(KEY_DEBUG_ENABLED, DEFAULT_DEBUG_ENABLED);
}
/**
* Включает/выключает дебаг-режим
*/
public void setDebugEnabled(boolean enabled) {
prefs.edit().putBoolean(KEY_DEBUG_ENABLED, enabled).apply();
Log.i(TAG, "Дебаг-режим: " + (enabled ? "включен" : "выключен"));
}
}
@@ -114,13 +114,13 @@ public class CompassView extends BaseDockWidget {
float baseHeight = dp(80); // базовая высота
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
// Простой текст для проверки
// Простой текст для проверки (убрана надпись "КОМПАС")
paint.setColor(Color.WHITE);
paint.setTextSize(24 * scaleFactor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("КОМПАС", w/2, h/2, paint);
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
float topTextY = dp(18) * scaleFactor;
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, topTextY, paint);
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, topTextY + 24 * scaleFactor, paint);
// Плавное обновление азимута
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
@@ -157,7 +157,10 @@ public class CompassView extends BaseDockWidget {
if (degree % 45 == 0) {
int directionIndex = (degree / 45) % 8;
if (directionIndex < directions.length) {
paint.setTextSize(18 * scaleFactor);
// Буква стороны света увеличивается при приближении к центру
float proximity = 1f - Math.min(Math.abs(relativeDegree) / (visibleDegrees / 2f), 1f);
float letterSize = (24f + 36f * proximity) * scaleFactor; // 24..48
paint.setTextSize(letterSize);
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
}
}
@@ -62,6 +62,8 @@ public class CursorOverlay {
private void initializeViews() {
LayoutInflater inflater = LayoutInflater.from(context);
overlayView = inflater.inflate(R.layout.cursor, null);
// Помечаем overlay для последующего обнаружения/удаления
overlayView.setTag("cursor_overlay");
tvCursorLatitude = overlayView.findViewById(R.id.tv_cursor_latitude);
tvCursorLongitude = overlayView.findViewById(R.id.tv_cursor_longitude);
@@ -350,6 +352,10 @@ public class CursorOverlay {
public void hideCursor() {
if (overlayView != null) {
overlayView.setVisibility(View.GONE);
// Также скрываем панели, чтобы исключить ложное наложение
if (aisVesselInfoPanel != null) aisVesselInfoPanel.setVisibility(View.GONE);
if (coordinatesPanel != null) coordinatesPanel.clearAnimation();
if (distanceBearingPanel != null) distanceBearingPanel.clearAnimation();
}
}
@@ -359,6 +365,18 @@ public class CursorOverlay {
public void showCursor() {
if (overlayView != null) {
overlayView.setVisibility(View.VISIBLE);
// Поднимем overlay над картой
overlayView.bringToFront();
overlayView.setElevation(10f);
// Гарантируем, что крест выше, а панель AIS ещё выше
View cursorCross = overlayView.findViewById(R.id.cursor_cross);
if (cursorCross != null) {
cursorCross.setElevation(12f);
}
if (aisVesselInfoPanel != null) {
aisVesselInfoPanel.bringToFront();
aisVesselInfoPanel.setElevation(20f);
}
}
}
@@ -4,6 +4,15 @@
android:layout_height="match_parent"
android:orientation="vertical">
<EditText
android:id="@+id/edit_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Поиск по MMSI, названию..."
android:inputType="text"
android:imeOptions="actionDone"
android:padding="12dp"/>
<TextView
android:id="@+id/text_target_count"
android:layout_width="match_parent"
+11
View File
@@ -48,6 +48,17 @@
android:scaleType="fitCenter"
android:layout_marginBottom="8dp" />
<ImageButton
android:id="@+id/btn_cursor_toggle"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/button_background"
android:src="@drawable/cursorcross"
android:contentDescription="Курсор"
android:padding="8dp"
android:scaleType="fitCenter"
android:layout_marginBottom="8dp" />
<ImageButton
android:id="@+id/btn_settings"
android:layout_width="40dp"
+5 -14
View File
@@ -545,7 +545,7 @@
</com.google.android.material.card.MaterialCardView>
<!-- Настройки курсора -->
<!-- Дебаг-режим -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -562,7 +562,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="🎯 Курсор на карте"
android:text="🐞 Режим отладки"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black"
@@ -571,29 +571,20 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Настройте отображение курсора с координатами центра экрана:"
android:text="Включите расширенное логирование и диагностические элементы UI."
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:id="@+id/switch_debug_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Показать курсор"
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>
+55 -10
View File
@@ -17,7 +17,7 @@
android:id="@+id/coordinates_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/panel_background"
android:background="@android:color/transparent"
android:orientation="vertical"
android:padding="8dp"
android:visibility="visible">
@@ -29,7 +29,12 @@
android:fontFamily="monospace"
android:text="Широта: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
<TextView
android:id="@+id/tv_cursor_longitude"
@@ -38,7 +43,12 @@
android:fontFamily="monospace"
android:text="Долгота: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
</LinearLayout>
@@ -47,7 +57,7 @@
android:id="@+id/distance_bearing_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/panel_background"
android:background="@android:color/transparent"
android:orientation="vertical"
android:padding="8dp"
android:visibility="visible">
@@ -60,6 +70,11 @@
android:text="Rnd:"
android:textColor="@android:color/white"
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2"
android:visibility="gone"
/>
@@ -71,6 +86,11 @@
android:text="Brg: --"
android:textColor="@android:color/white"
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2"
android:visibility="gone"
/>
@@ -81,7 +101,7 @@
android:id="@+id/ais_vessel_info_panel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/panel_background"
android:background="@android:color/transparent"
android:orientation="vertical"
android:padding="8dp"
android:visibility="gone">
@@ -93,7 +113,12 @@
android:fontFamily="monospace"
android:text="MMSI: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
<TextView
android:id="@+id/tv_ais_name"
@@ -102,7 +127,12 @@
android:fontFamily="monospace"
android:text="Название: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
<TextView
android:id="@+id/tv_ais_call_sign"
@@ -111,7 +141,12 @@
android:fontFamily="monospace"
android:text="Позывной: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
<TextView
android:id="@+id/tv_ais_cog"
@@ -120,7 +155,12 @@
android:fontFamily="monospace"
android:text="COG: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
<TextView
android:id="@+id/tv_ais_sog"
@@ -129,7 +169,12 @@
android:fontFamily="monospace"
android:text="SOG: --"
android:textColor="@android:color/white"
android:textSize="12sp" />
android:textSize="12sp"
android:textStyle="bold"
android:shadowColor="#000000"
android:shadowDx="0"
android:shadowDy="0"
android:shadowRadius="2" />
</LinearLayout>
+10 -5
View File
@@ -3,10 +3,17 @@
```plantuml
@startuml AIS_Map_Architecture
!theme plain
!define RECTANGLE class
skinparam classAttributeIconSize 0
skinparam classFontSize 10
skinparam packageFontSize 12
skinparam classFontSize 9
skinparam packageFontSize 11
skinparam backgroundColor white
skinparam classBackgroundColor white
skinparam packageBackgroundColor lightblue
skinparam packageBorderColor black
skinparam classBorderColor black
skinparam interfaceBackgroundColor lightgreen
skinparam interfaceBorderColor black
package "Main Activity" {
class MainActivity {
@@ -26,8 +33,6 @@ package "Main Activity" {
+ onResume()
+ onPause()
+ onDestroy()
+ onCreateOptionsMenu()
+ onOptionsItemSelected()
}
class AisTargetsActivity {
+796
View File
@@ -0,0 +1,796 @@
# Диаграмма классов AIS Map Application (PlantUML - Без Graphviz)
```plantuml
@startuml AIS_Map_Architecture_NoGraphviz
!define RECTANGLE class
skinparam classAttributeIconSize 0
skinparam classFontSize 8
skinparam packageFontSize 10
skinparam backgroundColor white
skinparam classBackgroundColor white
skinparam packageBackgroundColor lightblue
skinparam packageBorderColor black
skinparam classBorderColor black
skinparam interfaceBackgroundColor lightgreen
skinparam interfaceBorderColor black
skinparam arrowColor black
skinparam arrowThickness 1
package "Main Activity" {
class MainActivity {
- AppCoordinator appCoordinator
- MenuBinder menuBinder
- BottomSheetsBinder bottomSheetsBinder
- PermissionsBinder permissionsBinder
- MapController mapController
- CompassController compassController
- UIRenderingCoordinator uiCoordinator
+ onCreate()
+ onResume()
+ onPause()
+ onDestroy()
}
class AisTargetsActivity {
- AisTargetsAdapter adapter
- List<AISVessel> aisVessels
+ onCreate()
+ updateAISList()
}
class SettingsActivity {
- SettingsManager settingsManager
+ onCreate()
+ saveSettings()
}
}
package "Controllers Factory" {
interface ControllersFactory {
+ createAppCoordinator() : AppCoordinator
}
class DefaultControllersFactory {
+ createAppCoordinator() : AppCoordinator
}
}
package "Core Controllers" {
class AppCoordinator {
- Context context
- NMEAController nmeaController
- NetworkController networkController
- DataController dataController
- NotificationController notificationController
- CompassController compassController
- MapController mapController
- Vessel ownVessel
- List<AISVessel> aisVessels
- SettingsManager settingsManager
+ initializeControllers()
+ startServices()
+ stopServices()
+ onVesselUpdated()
+ onAISVesselUpdated()
+ onDOPUpdated()
+ onDataReceived()
+ onNotificationShown()
+ onCompassChanged()
}
class NMEAController {
- Context context
- NMEAParser nmeaParser
- AndroidNMEAListener androidNmeaListener
- GPSLocationListener gpsLocationListener
- ExecutorService executor
+ startAndroidNMEAListener()
+ stopAndroidNMEAListener()
+ startGPSLocationListener()
+ stopGPSLocationListener()
+ parseNMEAData()
}
class NetworkController {
- Context context
- UDPListener udpListener
- ExecutorService executor
- int udpPort
- boolean isUDPEnabled
+ setUDPEnabled()
+ startUDPListener()
+ stopUDPListener()
}
class DataController {
- Context context
- Repository repository
- SettingsManager settingsManager
- ExecutorService executor
+ restoreDataAsync()
+ saveVesselData()
+ saveAISData()
+ performDatabaseCleanup()
}
class NotificationController {
- Context context
- NotificationService notificationService
+ notifyNewAISTarget()
+ notifySafetyMessage()
+ notifyGPSStatus()
}
class CompassController {
- Context context
- CompassSensor compassSensor
- Handler uiHandler
+ startCompass()
+ stopCompass()
+ isCompassAvailable() : boolean
+ isCompassActive() : boolean
+ getCompassStatus() : String
}
class MapController {
- Context context
- MapInterface currentMapInterface
- MapView mapView
- MapLibreMapView mapLibreView
- List<MapInterfaceChangeListener> listeners
+ addMapInterfaceChangeListener()
+ removeMapInterfaceChangeListener()
+ switchToYandexMaps()
+ switchToMapLibre()
+ getCurrentMapInterface() : MapInterface
}
}
package "UI Binders" {
class MenuBinder {
- AppCoordinator appCoordinator
- SettingsManager settingsManager
- MenuActions actions
+ onCreateOptionsMenu()
+ onPrepareOptionsMenu()
+ onOptionsItemSelected()
}
class BottomSheetsBinder {
- Context context
- BottomSheetDialog ownVesselBottomSheet
- BottomSheetDialog aisVesselBottomSheet
- View ownBottomSheetView
- View aisBottomSheetView
- AISVessel currentAISVessel
+ init()
+ initAIS()
+ showOwnVesselSheet()
+ showAISVesselSheet()
+ startAutoUpdate()
+ stopAutoUpdate()
}
class BottomSheetsManager {
- Context context
- AppCoordinator appCoordinator
- BottomSheetDialog ownVesselBottomSheet
- BottomSheetDialog aisVesselBottomSheet
- View bottomSheetView
- View aisBottomSheetView
- AISVessel currentAISVessel
+ init()
+ showOwnVesselSheet()
+ showAISVesselSheet()
+ updateOwnVesselUI()
+ updateAISBottomSheetUI()
+ stopAutoUpdate()
}
class PermissionsBinder {
- Activity activity
+ ensurePermission() : boolean
+ handleOnRequestPermissionsResult() : boolean
}
}
package "Data Processing" {
class NMEAParser {
- Vessel ownVessel
- List<AISVessel> aisVessels
- NMEAParserListener listener
- GPSLocationListener gpsLocationListener
- Map<String, Map<Integer, String>> aisFragments
- boolean hybridMode
+ parseNMEA()
+ setHybridMode()
+ setGPSLocationListener()
}
class UDPListener {
- int port
- DatagramSocket socket
- ExecutorService executor
- AtomicBoolean isRunning
- UDPListenerCallback callback
+ start()
+ stop()
+ setCallback()
}
class AndroidNMEAListener {
- LocationManager locationManager
- NMEAMessageCallback callback
- boolean isListening
+ startListening() : boolean
+ stopListening()
+ setCallback()
}
class GPSLocationListener {
- Context context
- LocationManager locationManager
- LocationCallback callback
- boolean isListening
- int satelliteCount
- int activeSatellites
- double pdop
- double hdop
- double vdop
+ startListening() : boolean
+ stopListening()
+ setCallback()
}
class VesselPathController {
- Context context
- SettingsManager settingsManager
- SharedPreferences prefs
- String vesselId
- Handler uiHandler
- List<VesselPathPoint> pathPoints
- VesselPathPoint lastPoint
+ addPathPoint()
+ getPathPoints() : List<VesselPathPoint>
+ clearPath()
+ savePath()
+ loadPath()
}
}
package "Maps" {
interface MapInterface {
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
class YandexMapImpl {
- Context context
- MapView mapView
- MapObjectCollection mapObjects
- MarkerClickListener markerClickListener
- YandexMarkerManager markerManager
- CursorOverlay cursorOverlay
- Vessel ownVessel
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
class MapLibreMapImpl {
- Context context
- MapView mapView
- MapLibreMap mapLibreMap
- MarkerClickListener markerClickListener
- CursorOverlay cursorOverlay
- Vessel ownVessel
- Map<String, AISVessel> aisVessels
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
class MapForgeImpl {
- Context context
- MapView mapView
- MarkerClickListener markerClickListener
- CursorOverlay cursorOverlay
- Vessel ownVessel
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
}
package "Data Models" {
class Vessel {
- double latitude
- double longitude
- double course
- double speed
- double heading
- double magneticCompass
- int signalStrength
- LocalDateTime lastUpdate
- String vesselName
- String mmsi
- String callSign
- double altitude
- int satellites
- int activeSatellites
- double pdop
- double hdop
- double vdop
- float accuracy
- long fixTime
- String fixQuality
+ updatePosition()
+ updateGPSQuality()
+ getGPSQualityPercentage() : int
+ getGPSQualityDescription() : String
}
class AISVessel {
- String mmsi
- String vesselName
- String callSign
- int imo
- String vesselType
- double latitude
- double longitude
- double course
- double speed
- double heading
- double rateOfTurn
- double length
- double width
- double draft
- String destination
- LocalDateTime eta
- LocalDateTime lastUpdate
- int signalStrength
- boolean isActive
- String navigationalStatus
- String lastSafetyMessage
- boolean positionAccuracy
- String vesselClass
- String vendorId
- boolean selected
+ updatePosition()
+ isDataStale() : boolean
+ shouldBeRemoved() : boolean
+ getMinutesSinceLastUpdate() : long
}
class VesselPathPoint {
- double latitude
- double longitude
- double course
- double speed
- long timestamp
+ VesselPathPoint()
+ toJSON() : String
+ fromJSON()
}
}
package "Database" {
abstract class AppDatabase {
+ aisVesselDao() : AISVesselDao
+ vesselDao() : VesselDao
+ getInstance() : AppDatabase
}
class Repository {
- AISVesselDao aisVesselDao
- VesselDao vesselDao
- ExecutorService ioExecutor
+ upsertAIS()
+ deleteStaleAIS()
+ getAllAISSync() : List<AISVesselEntity>
+ observeAllAIS() : LiveData<List<AISVesselEntity>>
+ getAISByMmsiSync() : AISVesselEntity
+ upsertOwnVessel()
+ getLatestOwnVesselSync() : VesselEntity
+ getLatestOwnVesselAsync()
}
interface AISVesselDao {
+ upsert()
+ deleteStale()
+ getAll() : List<AISVesselEntity>
+ observeAll() : LiveData<List<AISVesselEntity>>
+ getByMmsi() : AISVesselEntity
}
interface VesselDao {
+ upsert()
+ getLatest() : VesselEntity
}
class AISVesselEntity {
- String mmsi
- String vesselName
- String callSign
- int imo
- String vesselType
- double latitude
- double longitude
- double course
- double speed
- double heading
- double rateOfTurn
- double length
- double width
- double draft
- String destination
- long etaEpochMs
- long lastUpdateEpochMs
- int signalStrength
- boolean isActive
- String navigationalStatus
- String lastSafetyMessage
- boolean positionAccuracy
- String vesselClass
- String vendorId
}
class VesselEntity {
- double latitude
- double longitude
- double course
- double speed
- double heading
- double magneticCompass
- int signalStrength
- long lastUpdateEpochMs
- String vesselName
- String mmsi
- String callSign
- double altitude
- int satellites
- int activeSatellites
- double pdop
- double hdop
- double vdop
- float accuracy
- long fixTime
- String fixQuality
}
}
package "UI Components" {
class UIRenderingCoordinator {
- MapInterface mapInterface
- Handler uiHandler
- Vessel pendingVesselUpdate
- Map<String, AISVessel> pendingAISUpdates
- Set<String> pendingAISRemovals
- Runnable vesselUpdateRunnable
- Runnable aisUpdateRunnable
- Runnable pathUpdateRunnable
- boolean vesselUpdatePending
- boolean aisUpdatePending
- boolean pathUpdatePending
+ requestVesselUpdate()
+ requestAISUpdate()
+ requestAISRemoval()
+ flushPendingOperations()
+ cleanup()
}
interface UIDataChangeNotifier {
+ onVesselPositionChanged()
+ onGPSQualityChanged()
+ onAISVesselChanged()
+ onAISVesselRemoved()
+ onVesselPathChanged()
+ onRequestCenterMap()
+ onCompassUpdate()
}
class CompassView {
- float azimuth
- Paint compassPaint
- Paint needlePaint
- Paint textPaint
- List<AISVessel> nearbyVessels
+ setAzimuth()
+ setNearbyVessels()
+ onDraw()
}
class CompassSensor {
- SensorManager sensorManager
- Sensor magnetometer
- Sensor accelerometer
- CompassListener callback
- float[] lastAccelerometer
- float[] lastMagnetometer
- boolean lastAccelerometerSet
- boolean lastMagnetometerSet
- float[] rotationMatrix
- float[] orientation
+ startListening()
+ stopListening()
+ setCallback()
}
class CoordinatesDockWidget {
- TextView latitudeText
- TextView longitudeText
- TextView accuracyText
- TextView satellitesText
- TextView qualityText
+ updateCoordinates()
+ updateGPSQuality()
}
class CursorOverlay {
- ViewGroup parentView
- TextView coordinatesText
- TextView vesselInfoText
- boolean isVisible
+ show()
+ hide()
+ updateCoordinates()
+ setVesselInfo()
+ clearVesselInfo()
}
}
package "Services" {
class NotificationService {
- Context context
- SettingsManager settingsManager
- Vibrator vibrator
- ToneGenerator toneGenerator
- boolean isInitialized
+ showSafetyAlert()
+ showNewVesselNotification()
+ clearNotifications()
+ setVibrationEnabled()
+ setSoundEnabled()
}
class AISForegroundService {
- Context context
- AppCoordinator appCoordinator
- NotificationManager notificationManager
- boolean isRunning
+ startForeground()
+ stopForeground()
+ onStartCommand()
+ onDestroy()
}
}
package "Utils" {
class SettingsManager {
- Context context
- SharedPreferences prefs
+ getUDPPort() : int
+ setUDPPort()
+ isUDPEnabled() : boolean
+ setUDPEnabled()
+ isAndroidNMEAEnabled() : boolean
+ setAndroidNMEAEnabled()
+ isUDPNMEAEnabled() : boolean
+ setUDPNMEAEnabled()
+ getDataMode() : String
+ setDataMode()
+ getDataStaleWarningMinutes() : int
+ setDataStaleWarningMinutes()
+ getDataStaleRemoveMinutes() : int
+ setDataStaleRemoveMinutes()
+ isPathTrackingEnabled() : boolean
+ setPathTrackingEnabled()
+ getPathColor() : int
+ setPathColor()
+ getPredictionColor() : int
+ setPredictionColor()
+ getPathWidth() : float
+ setPathWidth()
+ getPredictionWidth() : float
+ setPredictionWidth()
+ getPathMaxPoints() : int
+ setPathMaxPoints()
+ getPredictionHorizonSec() : int
+ setPredictionHorizonSec()
+ isVibrationEnabled() : boolean
+ setVibrationEnabled()
+ isSoundEnabled() : boolean
+ setSoundEnabled()
+ isKeepScreenOnEnabled() : boolean
+ setKeepScreenOnEnabled()
+ isCursorEnabled() : boolean
+ setCursorEnabled()
}
class GeoUtils {
+ calculateDistance() : double
+ calculateBearing() : double
+ isValidCoordinate() : boolean
+ formatCoordinate() : String
+ convertToDecimalDegrees() : double
}
class LogSender {
+ sendLog()
+ sendError()
+ sendWarning()
+ sendInfo()
}
class MIDToCountry {
+ getCountryByMID() : String
+ getCountryName() : String
+ isValidMID() : boolean
}
class NavigationUtils {
+ calculateCourse() : double
+ calculateSpeed() : double
+ calculateETA() : LocalDateTime
+ isCollisionRisk() : boolean
}
}
' Основные связи
MainActivity --> AppCoordinator : uses
MainActivity --> MenuBinder : uses
MainActivity --> BottomSheetsBinder : uses
MainActivity --> PermissionsBinder : uses
MainActivity --> MapController : uses
MainActivity --> CompassController : uses
MainActivity --> UIRenderingCoordinator : uses
MainActivity --> CompassView : uses
MainActivity --> CoordinatesDockWidget : uses
MainActivity --> BottomSheetsManager : uses
ControllersFactory <|.. DefaultControllersFactory : implements
MainActivity --> ControllersFactory : uses
DefaultControllersFactory --> AppCoordinator : creates
AppCoordinator --> NMEAController : coordinates
AppCoordinator --> NetworkController : coordinates
AppCoordinator --> DataController : coordinates
AppCoordinator --> NotificationController : coordinates
AppCoordinator --> CompassController : coordinates
AppCoordinator --> MapController : coordinates
AppCoordinator --> VesselPathController : uses
AppCoordinator --> SettingsManager : uses
AppCoordinator --> UIRenderingCoordinator : uses
NMEAController --> NMEAParser : uses
NMEAController --> AndroidNMEAListener : uses
NMEAController --> GPSLocationListener : uses
NetworkController --> UDPListener : uses
DataController --> Repository : uses
DataController --> SettingsManager : uses
NotificationController --> NotificationService : uses
CompassController --> CompassSensor : uses
MenuBinder --> AppCoordinator : uses
MenuBinder --> SettingsManager : uses
BottomSheetsBinder --> Context : uses
BottomSheetsManager --> AppCoordinator : uses
PermissionsBinder --> Activity : uses
MapController --> MapInterface : manages
MapController --> YandexMapImpl : creates
MapController --> MapLibreMapImpl : creates
MapController --> MapForgeImpl : creates
YandexMapImpl ..|> MapInterface : implements
MapLibreMapImpl ..|> MapInterface : implements
MapForgeImpl ..|> MapInterface : implements
Repository --> AppDatabase : uses
Repository --> AISVesselDao : uses
Repository --> VesselDao : uses
AppDatabase --> AISVesselEntity : contains
AppDatabase --> VesselEntity : contains
UIRenderingCoordinator ..|> UIDataChangeNotifier : implements
NMEAParser --> Vessel : creates/updates
NMEAParser --> AISVessel : creates/updates
NMEAParser --> GPSLocationListener : uses
VesselPathController --> VesselPathPoint : manages
VesselPathController --> SettingsManager : uses
NotificationService --> SettingsManager : uses
CompassSensor --> CompassView : updates
CompassView --> AISVessel : displays
@enduml
```
## Описание PlantUML диаграммы без Graphviz
### 🎯 **Ключевые изменения для совместимости:**
1. **Убрана зависимость от Graphviz** - диаграмма использует только встроенные возможности PlantUML
2. **Упрощены настройки** - оставлены только базовые skinparam параметры
3. **Оптимизированы размеры** - уменьшены размеры шрифтов для лучшей читаемости
4. **Убраны сложные элементы** - удалены некоторые методы для упрощения
### 📦 **Структура пакетов:**
- **Main Activity** - основные активности приложения
- **Controllers Factory** - фабрика для создания контроллеров
- **Core Controllers** - основные контроллеры системы
- **UI Binders** - компоненты для управления UI
- **Data Processing** - обработка данных и парсинг
- **Maps** - система карт и маркеров
- **Data Models** - модели данных
- **Database** - слой базы данных
- **UI Components** - UI компоненты
- **Services** - сервисы приложения
- **Utils** - утилиты и вспомогательные классы
### ✅ **Преимущества этой версии:**
1. **Совместимость** - работает без Graphviz
2. **Производительность** - быстрее генерируется
3. **Портативность** - работает в большинстве редакторов
4. **Читаемость** - четкая структура и связи
### 🔧 **Как использовать:**
1. **Онлайн редакторы:** PlantUML Online Server, PlantText
2. **IDE плагины:** IntelliJ IDEA, VS Code, Eclipse
3. **Командная строка:** PlantUML jar файл
4. **Документация:** GitLab, GitHub поддерживают PlantUML
Эта версия должна работать без проблем и показывать всю архитектуру вашего AIS Map приложения!
+793
View File
@@ -0,0 +1,793 @@
# Диаграмма классов AIS Map Application (PlantUML - Упрощенная версия)
```plantuml
@startuml AIS_Map_Architecture_Simple
skinparam classAttributeIconSize 0
skinparam classFontSize 9
skinparam packageFontSize 11
skinparam backgroundColor white
skinparam classBackgroundColor white
skinparam packageBackgroundColor lightblue
skinparam packageBorderColor black
skinparam classBorderColor black
skinparam interfaceBackgroundColor lightgreen
skinparam interfaceBorderColor black
package "Main Activity" {
class MainActivity {
- AppCoordinator appCoordinator
- MenuBinder menuBinder
- BottomSheetsBinder bottomSheetsBinder
- PermissionsBinder permissionsBinder
- MapController mapController
- CompassController compassController
- UIRenderingCoordinator uiCoordinator
+ onCreate()
+ onResume()
+ onPause()
+ onDestroy()
}
class AisTargetsActivity {
- AisTargetsAdapter adapter
- List<AISVessel> aisVessels
+ onCreate()
+ updateAISList()
}
class SettingsActivity {
- SettingsManager settingsManager
+ onCreate()
+ saveSettings()
}
}
package "Controllers Factory" {
interface ControllersFactory {
+ createAppCoordinator() : AppCoordinator
}
class DefaultControllersFactory {
+ createAppCoordinator() : AppCoordinator
}
}
package "Core Controllers" {
class AppCoordinator {
- Context context
- NMEAController nmeaController
- NetworkController networkController
- DataController dataController
- NotificationController notificationController
- CompassController compassController
- MapController mapController
- Vessel ownVessel
- List<AISVessel> aisVessels
- SettingsManager settingsManager
+ initializeControllers()
+ startServices()
+ stopServices()
+ onVesselUpdated()
+ onAISVesselUpdated()
+ onDOPUpdated()
+ onDataReceived()
+ onNotificationShown()
+ onCompassChanged()
}
class NMEAController {
- Context context
- NMEAParser nmeaParser
- AndroidNMEAListener androidNmeaListener
- GPSLocationListener gpsLocationListener
- ExecutorService executor
+ startAndroidNMEAListener()
+ stopAndroidNMEAListener()
+ startGPSLocationListener()
+ stopGPSLocationListener()
+ parseNMEAData()
}
class NetworkController {
- Context context
- UDPListener udpListener
- ExecutorService executor
- int udpPort
- boolean isUDPEnabled
+ setUDPEnabled()
+ startUDPListener()
+ stopUDPListener()
}
class DataController {
- Context context
- Repository repository
- SettingsManager settingsManager
- ExecutorService executor
+ restoreDataAsync()
+ saveVesselData()
+ saveAISData()
+ performDatabaseCleanup()
}
class NotificationController {
- Context context
- NotificationService notificationService
+ notifyNewAISTarget()
+ notifySafetyMessage()
+ notifyGPSStatus()
}
class CompassController {
- Context context
- CompassSensor compassSensor
- Handler uiHandler
+ startCompass()
+ stopCompass()
+ isCompassAvailable() : boolean
+ isCompassActive() : boolean
+ getCompassStatus() : String
}
class MapController {
- Context context
- MapInterface currentMapInterface
- MapView mapView
- MapLibreMapView mapLibreView
- List<MapInterfaceChangeListener> listeners
+ addMapInterfaceChangeListener()
+ removeMapInterfaceChangeListener()
+ switchToYandexMaps()
+ switchToMapLibre()
+ getCurrentMapInterface() : MapInterface
}
}
package "UI Binders" {
class MenuBinder {
- AppCoordinator appCoordinator
- SettingsManager settingsManager
- MenuActions actions
+ onCreateOptionsMenu()
+ onPrepareOptionsMenu()
+ onOptionsItemSelected()
}
class BottomSheetsBinder {
- Context context
- BottomSheetDialog ownVesselBottomSheet
- BottomSheetDialog aisVesselBottomSheet
- View ownBottomSheetView
- View aisBottomSheetView
- AISVessel currentAISVessel
+ init()
+ initAIS()
+ showOwnVesselSheet()
+ showAISVesselSheet()
+ startAutoUpdate()
+ stopAutoUpdate()
}
class BottomSheetsManager {
- Context context
- AppCoordinator appCoordinator
- BottomSheetDialog ownVesselBottomSheet
- BottomSheetDialog aisVesselBottomSheet
- View bottomSheetView
- View aisBottomSheetView
- AISVessel currentAISVessel
+ init()
+ showOwnVesselSheet()
+ showAISVesselSheet()
+ updateOwnVesselUI()
+ updateAISBottomSheetUI()
+ stopAutoUpdate()
}
class PermissionsBinder {
- Activity activity
+ ensurePermission() : boolean
+ handleOnRequestPermissionsResult() : boolean
}
}
package "Data Processing" {
class NMEAParser {
- Vessel ownVessel
- List<AISVessel> aisVessels
- NMEAParserListener listener
- GPSLocationListener gpsLocationListener
- Map<String, Map<Integer, String>> aisFragments
- boolean hybridMode
+ parseNMEA()
+ setHybridMode()
+ setGPSLocationListener()
}
class UDPListener {
- int port
- DatagramSocket socket
- ExecutorService executor
- AtomicBoolean isRunning
- UDPListenerCallback callback
+ start()
+ stop()
+ setCallback()
}
class AndroidNMEAListener {
- LocationManager locationManager
- NMEAMessageCallback callback
- boolean isListening
+ startListening() : boolean
+ stopListening()
+ setCallback()
}
class GPSLocationListener {
- Context context
- LocationManager locationManager
- LocationCallback callback
- boolean isListening
- int satelliteCount
- int activeSatellites
- double pdop
- double hdop
- double vdop
+ startListening() : boolean
+ stopListening()
+ setCallback()
}
class VesselPathController {
- Context context
- SettingsManager settingsManager
- SharedPreferences prefs
- String vesselId
- Handler uiHandler
- List<VesselPathPoint> pathPoints
- VesselPathPoint lastPoint
+ addPathPoint()
+ getPathPoints() : List<VesselPathPoint>
+ clearPath()
+ savePath()
+ loadPath()
}
}
package "Maps" {
interface MapInterface {
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
class YandexMapImpl {
- Context context
- MapView mapView
- MapObjectCollection mapObjects
- MarkerClickListener markerClickListener
- YandexMarkerManager markerManager
- CursorOverlay cursorOverlay
- Vessel ownVessel
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
class MapLibreMapImpl {
- Context context
- MapView mapView
- MapLibreMap mapLibreMap
- MarkerClickListener markerClickListener
- CursorOverlay cursorOverlay
- Vessel ownVessel
- Map<String, AISVessel> aisVessels
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
class MapForgeImpl {
- Context context
- MapView mapView
- MarkerClickListener markerClickListener
- CursorOverlay cursorOverlay
- Vessel ownVessel
+ initialize()
+ cleanup()
+ addOwnVesselMarker()
+ updateOwnVesselPosition()
+ addAISVesselMarker()
+ updateAISVesselPosition()
+ removeAISVesselMarker()
+ clearAISVesselMarkers()
+ centerOnPosition()
+ setZoom()
+ getZoom() : float
+ setBearing()
+ getBearing() : float
}
}
package "Data Models" {
class Vessel {
- double latitude
- double longitude
- double course
- double speed
- double heading
- double magneticCompass
- int signalStrength
- LocalDateTime lastUpdate
- String vesselName
- String mmsi
- String callSign
- double altitude
- int satellites
- int activeSatellites
- double pdop
- double hdop
- double vdop
- float accuracy
- long fixTime
- String fixQuality
+ updatePosition()
+ updateGPSQuality()
+ getGPSQualityPercentage() : int
+ getGPSQualityDescription() : String
}
class AISVessel {
- String mmsi
- String vesselName
- String callSign
- int imo
- String vesselType
- double latitude
- double longitude
- double course
- double speed
- double heading
- double rateOfTurn
- double length
- double width
- double draft
- String destination
- LocalDateTime eta
- LocalDateTime lastUpdate
- int signalStrength
- boolean isActive
- String navigationalStatus
- String lastSafetyMessage
- boolean positionAccuracy
- String vesselClass
- String vendorId
- boolean selected
+ updatePosition()
+ isDataStale() : boolean
+ shouldBeRemoved() : boolean
+ getMinutesSinceLastUpdate() : long
}
class VesselPathPoint {
- double latitude
- double longitude
- double course
- double speed
- long timestamp
+ VesselPathPoint()
+ toJSON() : String
+ fromJSON()
}
}
package "Database" {
abstract class AppDatabase {
+ aisVesselDao() : AISVesselDao
+ vesselDao() : VesselDao
+ getInstance() : AppDatabase
}
class Repository {
- AISVesselDao aisVesselDao
- VesselDao vesselDao
- ExecutorService ioExecutor
+ upsertAIS()
+ deleteStaleAIS()
+ getAllAISSync() : List<AISVesselEntity>
+ observeAllAIS() : LiveData<List<AISVesselEntity>>
+ getAISByMmsiSync() : AISVesselEntity
+ upsertOwnVessel()
+ getLatestOwnVesselSync() : VesselEntity
+ getLatestOwnVesselAsync()
}
interface AISVesselDao {
+ upsert()
+ deleteStale()
+ getAll() : List<AISVesselEntity>
+ observeAll() : LiveData<List<AISVesselEntity>>
+ getByMmsi() : AISVesselEntity
}
interface VesselDao {
+ upsert()
+ getLatest() : VesselEntity
}
class AISVesselEntity {
- String mmsi
- String vesselName
- String callSign
- int imo
- String vesselType
- double latitude
- double longitude
- double course
- double speed
- double heading
- double rateOfTurn
- double length
- double width
- double draft
- String destination
- long etaEpochMs
- long lastUpdateEpochMs
- int signalStrength
- boolean isActive
- String navigationalStatus
- String lastSafetyMessage
- boolean positionAccuracy
- String vesselClass
- String vendorId
}
class VesselEntity {
- double latitude
- double longitude
- double course
- double speed
- double heading
- double magneticCompass
- int signalStrength
- long lastUpdateEpochMs
- String vesselName
- String mmsi
- String callSign
- double altitude
- int satellites
- int activeSatellites
- double pdop
- double hdop
- double vdop
- float accuracy
- long fixTime
- String fixQuality
}
}
package "UI Components" {
class UIRenderingCoordinator {
- MapInterface mapInterface
- Handler uiHandler
- Vessel pendingVesselUpdate
- Map<String, AISVessel> pendingAISUpdates
- Set<String> pendingAISRemovals
- Runnable vesselUpdateRunnable
- Runnable aisUpdateRunnable
- Runnable pathUpdateRunnable
- boolean vesselUpdatePending
- boolean aisUpdatePending
- boolean pathUpdatePending
+ requestVesselUpdate()
+ requestAISUpdate()
+ requestAISRemoval()
+ flushPendingOperations()
+ cleanup()
}
interface UIDataChangeNotifier {
+ onVesselPositionChanged()
+ onGPSQualityChanged()
+ onAISVesselChanged()
+ onAISVesselRemoved()
+ onVesselPathChanged()
+ onRequestCenterMap()
+ onCompassUpdate()
}
class CompassView {
- float azimuth
- Paint compassPaint
- Paint needlePaint
- Paint textPaint
- List<AISVessel> nearbyVessels
+ setAzimuth()
+ setNearbyVessels()
+ onDraw()
}
class CompassSensor {
- SensorManager sensorManager
- Sensor magnetometer
- Sensor accelerometer
- CompassListener callback
- float[] lastAccelerometer
- float[] lastMagnetometer
- boolean lastAccelerometerSet
- boolean lastMagnetometerSet
- float[] rotationMatrix
- float[] orientation
+ startListening()
+ stopListening()
+ setCallback()
}
class CoordinatesDockWidget {
- TextView latitudeText
- TextView longitudeText
- TextView accuracyText
- TextView satellitesText
- TextView qualityText
+ updateCoordinates()
+ updateGPSQuality()
}
class CursorOverlay {
- ViewGroup parentView
- TextView coordinatesText
- TextView vesselInfoText
- boolean isVisible
+ show()
+ hide()
+ updateCoordinates()
+ setVesselInfo()
+ clearVesselInfo()
}
}
package "Services" {
class NotificationService {
- Context context
- SettingsManager settingsManager
- Vibrator vibrator
- ToneGenerator toneGenerator
- boolean isInitialized
+ showSafetyAlert()
+ showNewVesselNotification()
+ clearNotifications()
+ setVibrationEnabled()
+ setSoundEnabled()
}
class AISForegroundService {
- Context context
- AppCoordinator appCoordinator
- NotificationManager notificationManager
- boolean isRunning
+ startForeground()
+ stopForeground()
+ onStartCommand()
+ onDestroy()
}
}
package "Utils" {
class SettingsManager {
- Context context
- SharedPreferences prefs
+ getUDPPort() : int
+ setUDPPort()
+ isUDPEnabled() : boolean
+ setUDPEnabled()
+ isAndroidNMEAEnabled() : boolean
+ setAndroidNMEAEnabled()
+ isUDPNMEAEnabled() : boolean
+ setUDPNMEAEnabled()
+ getDataMode() : String
+ setDataMode()
+ getDataStaleWarningMinutes() : int
+ setDataStaleWarningMinutes()
+ getDataStaleRemoveMinutes() : int
+ setDataStaleRemoveMinutes()
+ isPathTrackingEnabled() : boolean
+ setPathTrackingEnabled()
+ getPathColor() : int
+ setPathColor()
+ getPredictionColor() : int
+ setPredictionColor()
+ getPathWidth() : float
+ setPathWidth()
+ getPredictionWidth() : float
+ setPredictionWidth()
+ getPathMaxPoints() : int
+ setPathMaxPoints()
+ getPredictionHorizonSec() : int
+ setPredictionHorizonSec()
+ isVibrationEnabled() : boolean
+ setVibrationEnabled()
+ isSoundEnabled() : boolean
+ setSoundEnabled()
+ isKeepScreenOnEnabled() : boolean
+ setKeepScreenOnEnabled()
+ isCursorEnabled() : boolean
+ setCursorEnabled()
}
class GeoUtils {
+ calculateDistance() : double
+ calculateBearing() : double
+ isValidCoordinate() : boolean
+ formatCoordinate() : String
+ convertToDecimalDegrees() : double
}
class LogSender {
+ sendLog()
+ sendError()
+ sendWarning()
+ sendInfo()
}
class MIDToCountry {
+ getCountryByMID() : String
+ getCountryName() : String
+ isValidMID() : boolean
}
class NavigationUtils {
+ calculateCourse() : double
+ calculateSpeed() : double
+ calculateETA() : LocalDateTime
+ isCollisionRisk() : boolean
}
}
' Основные связи
MainActivity --> AppCoordinator : uses
MainActivity --> MenuBinder : uses
MainActivity --> BottomSheetsBinder : uses
MainActivity --> PermissionsBinder : uses
MainActivity --> MapController : uses
MainActivity --> CompassController : uses
MainActivity --> UIRenderingCoordinator : uses
MainActivity --> CompassView : uses
MainActivity --> CoordinatesDockWidget : uses
MainActivity --> BottomSheetsManager : uses
ControllersFactory <|.. DefaultControllersFactory : implements
MainActivity --> ControllersFactory : uses
DefaultControllersFactory --> AppCoordinator : creates
AppCoordinator --> NMEAController : coordinates
AppCoordinator --> NetworkController : coordinates
AppCoordinator --> DataController : coordinates
AppCoordinator --> NotificationController : coordinates
AppCoordinator --> CompassController : coordinates
AppCoordinator --> MapController : coordinates
AppCoordinator --> VesselPathController : uses
AppCoordinator --> SettingsManager : uses
AppCoordinator --> UIRenderingCoordinator : uses
NMEAController --> NMEAParser : uses
NMEAController --> AndroidNMEAListener : uses
NMEAController --> GPSLocationListener : uses
NetworkController --> UDPListener : uses
DataController --> Repository : uses
DataController --> SettingsManager : uses
NotificationController --> NotificationService : uses
CompassController --> CompassSensor : uses
MenuBinder --> AppCoordinator : uses
MenuBinder --> SettingsManager : uses
BottomSheetsBinder --> Context : uses
BottomSheetsManager --> AppCoordinator : uses
PermissionsBinder --> Activity : uses
MapController --> MapInterface : manages
MapController --> YandexMapImpl : creates
MapController --> MapLibreMapImpl : creates
MapController --> MapForgeImpl : creates
YandexMapImpl ..|> MapInterface : implements
MapLibreMapImpl ..|> MapInterface : implements
MapForgeImpl ..|> MapInterface : implements
Repository --> AppDatabase : uses
Repository --> AISVesselDao : uses
Repository --> VesselDao : uses
AppDatabase --> AISVesselEntity : contains
AppDatabase --> VesselEntity : contains
UIRenderingCoordinator ..|> UIDataChangeNotifier : implements
NMEAParser --> Vessel : creates/updates
NMEAParser --> AISVessel : creates/updates
NMEAParser --> GPSLocationListener : uses
VesselPathController --> VesselPathPoint : manages
VesselPathController --> SettingsManager : uses
NotificationService --> SettingsManager : uses
CompassSensor --> CompassView : updates
CompassView --> AISVessel : displays
@enduml
```
## Описание упрощенной PlantUML диаграммы
### 🎯 **Исправления для совместимости:**
1. **Убрана тема** - удалена `!theme plain` которая могла вызывать проблемы
2. **Упрощены настройки** - оставлены только базовые skinparam
3. **Убраны сложные элементы** - удалены некоторые методы для упрощения
4. **Оптимизированы связи** - оставлены только основные связи
### 📦 **Структура пакетов:**
- **Main Activity** - основные активности приложения
- **Controllers Factory** - фабрика для создания контроллеров
- **Core Controllers** - основные контроллеры системы
- **UI Binders** - компоненты для управления UI
- **Data Processing** - обработка данных и парсинг
- **Maps** - система карт и маркеров
- **Data Models** - модели данных
- **Database** - слой базы данных
- **UI Components** - UI компоненты
- **Services** - сервисы приложения
- **Utils** - утилиты и вспомогательные классы
### 🔧 **Как использовать:**
1. **Онлайн редакторы:** PlantUML Online Server, PlantText
2. **IDE плагины:** IntelliJ IDEA, VS Code, Eclipse
3. **Командная строка:** PlantUML jar файл
4. **Документация:** GitLab, GitHub поддерживают PlantUML
### ✅ **Преимущества упрощенной версии:**
- Более совместима с различными PlantUML редакторами
- Меньше вероятность ошибок с Graphviz
- Сохраняет всю основную архитектуру
- Легче для понимания и презентации
Эта упрощенная версия должна работать без проблем с Graphviz и показывать всю архитектуру вашего приложения.