diff --git a/app/src/main/java/com/grigowashere/aismap/AisTargetsActivity.java b/app/src/main/java/com/grigowashere/aismap/AisTargetsActivity.java index 126d205..1236f14 100644 --- a/app/src/main/java/com/grigowashere/aismap/AisTargetsActivity.java +++ b/app/src/main/java/com/grigowashere/aismap/AisTargetsActivity.java @@ -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 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>() { @Override public void onChanged(List 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 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() { @Override diff --git a/app/src/main/java/com/grigowashere/aismap/MainActivity.java b/app/src/main/java/com/grigowashere/aismap/MainActivity.java index 3c873aa..1663108 100644 --- a/app/src/main/java/com/grigowashere/aismap/MainActivity.java +++ b/app/src/main/java/com/grigowashere/aismap/MainActivity.java @@ -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,12 +320,31 @@ 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-виджета @@ -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); - } - }); }); } @@ -904,6 +927,31 @@ public class MainActivity extends AppCompatActivity { Intent intent = new Intent(this, AisTargetsActivity.class); 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, "Требуется перезапуск сервисов"); diff --git a/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java b/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java index 38eac84..b379f7e 100644 --- a/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java +++ b/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java @@ -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); diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/AppCoordinator.java b/app/src/main/java/com/grigowashere/aismap/controllers/AppCoordinator.java index 6c2ff00..1db54ca 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/AppCoordinator.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/AppCoordinator.java @@ -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 aisVessels; + private Map aisVessels; private Map 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 getNearbyVessels() { List 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 getAISVessels() { synchronized (aisVessels) { - return new ArrayList<>(aisVessels); + return new ArrayList<>(aisVessels.values()); } } diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java index 807b0a1..a114893 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java @@ -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,10 +179,97 @@ 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,31 +516,57 @@ 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); + } + + // Поля 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); + } } - - // Убираем шумный лог GLL координат - + 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; diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java index 31862f3..2ce2fe2 100644 --- a/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java +++ b/app/src/main/java/com/grigowashere/aismap/maps/MapLibreMapImpl.java @@ -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 (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; - // } - // } - - // ЗАХАРДКОДИМ МАКСИМАЛЬНОЕ КОЛИЧЕСТВО ТОЧЕК ДЛЯ ТЕСТИРОВАНИЯ - int max = 5000; // settingsManager.getPathMaxPoints(); + // Проверяем, изменились ли координаты (строгая проверка на дублирование) + 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; + } + } + + // Лимит точек пути из настроек + 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,24 +2025,63 @@ 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 судно под курсором (в центре экрана) diff --git a/app/src/main/java/com/grigowashere/aismap/models/Vessel.java b/app/src/main/java/com/grigowashere/aismap/models/Vessel.java index 860be78..3097a36 100644 --- a/app/src/main/java/com/grigowashere/aismap/models/Vessel.java +++ b/app/src/main/java/com/grigowashere/aismap/models/Vessel.java @@ -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; } + /** * Обновляет данные судна */ diff --git a/app/src/main/java/com/grigowashere/aismap/ui/BottomSheetsManager.java b/app/src/main/java/com/grigowashere/aismap/ui/BottomSheetsManager.java index 3cd1cd0..d09c443 100644 --- a/app/src/main/java/com/grigowashere/aismap/ui/BottomSheetsManager.java +++ b/app/src/main/java/com/grigowashere/aismap/ui/BottomSheetsManager.java @@ -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()) : "🔒 Качество фикса: --"); } diff --git a/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java b/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java index 8451860..f4708a1 100644 --- a/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java +++ b/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java @@ -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"; @@ -468,5 +470,20 @@ public class SettingsManager { prefs.edit().putBoolean(KEY_NOTIFICATIONS_ENABLED, enabled).apply(); 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 ? "включен" : "выключен")); + } } diff --git a/app/src/main/java/com/grigowashere/aismap/view/CompassView.java b/app/src/main/java/com/grigowashere/aismap/view/CompassView.java index 4a630b4..fed753e 100644 --- a/app/src/main/java/com/grigowashere/aismap/view/CompassView.java +++ b/app/src/main/java/com/grigowashere/aismap/view/CompassView.java @@ -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); } } diff --git a/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java b/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java index f2f8ea8..b1fcdd0 100644 --- a/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java +++ b/app/src/main/java/com/grigowashere/aismap/view/CursorOverlay.java @@ -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); + } } } diff --git a/app/src/main/res/layout/activity_ais_targets.xml b/app/src/main/res/layout/activity_ais_targets.xml index 91a9165..5e6645a 100644 --- a/app/src/main/res/layout/activity_ais_targets.xml +++ b/app/src/main/res/layout/activity_ais_targets.xml @@ -4,6 +4,15 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + - + - - diff --git a/app/src/main/res/layout/cursor.xml b/app/src/main/res/layout/cursor.xml index a15208c..ea3175a 100644 --- a/app/src/main/res/layout/cursor.xml +++ b/app/src/main/res/layout/cursor.xml @@ -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" /> + android:textSize="12sp" + android:textStyle="bold" + android:shadowColor="#000000" + android:shadowDx="0" + android:shadowDy="0" + android:shadowRadius="2" /> @@ -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" /> + android:textSize="12sp" + android:textStyle="bold" + android:shadowColor="#000000" + android:shadowDx="0" + android:shadowDy="0" + android:shadowRadius="2" /> + android:textSize="12sp" + android:textStyle="bold" + android:shadowColor="#000000" + android:shadowDx="0" + android:shadowDy="0" + android:shadowRadius="2" /> + android:textSize="12sp" + android:textStyle="bold" + android:shadowColor="#000000" + android:shadowDx="0" + android:shadowDy="0" + android:shadowRadius="2" /> + android:textSize="12sp" + android:textStyle="bold" + android:shadowColor="#000000" + android:shadowDx="0" + android:shadowDy="0" + android:shadowRadius="2" /> diff --git a/class_diagram_plantuml.md b/class_diagram_plantuml.md index 2a2190a..d658757 100644 --- a/class_diagram_plantuml.md +++ b/class_diagram_plantuml.md @@ -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 { diff --git a/class_diagram_plantuml_nographviz.md b/class_diagram_plantuml_nographviz.md new file mode 100644 index 0000000..31820dd --- /dev/null +++ b/class_diagram_plantuml_nographviz.md @@ -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 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 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 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 aisVessels + - NMEAParserListener listener + - GPSLocationListener gpsLocationListener + - Map> 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 pathPoints + - VesselPathPoint lastPoint + + addPathPoint() + + getPathPoints() : List + + 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 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 + + observeAllAIS() : LiveData> + + getAISByMmsiSync() : AISVesselEntity + + upsertOwnVessel() + + getLatestOwnVesselSync() : VesselEntity + + getLatestOwnVesselAsync() + } + + interface AISVesselDao { + + upsert() + + deleteStale() + + getAll() : List + + observeAll() : LiveData> + + 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 pendingAISUpdates + - Set 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 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 приложения! diff --git a/class_diagram_plantuml_simple.md b/class_diagram_plantuml_simple.md new file mode 100644 index 0000000..100c2d4 --- /dev/null +++ b/class_diagram_plantuml_simple.md @@ -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 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 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 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 aisVessels + - NMEAParserListener listener + - GPSLocationListener gpsLocationListener + - Map> 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 pathPoints + - VesselPathPoint lastPoint + + addPathPoint() + + getPathPoints() : List + + 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 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 + + observeAllAIS() : LiveData> + + getAISByMmsiSync() : AISVesselEntity + + upsertOwnVessel() + + getLatestOwnVesselSync() : VesselEntity + + getLatestOwnVesselAsync() + } + + interface AISVesselDao { + + upsert() + + deleteStale() + + getAll() : List + + observeAll() : LiveData> + + 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 pendingAISUpdates + - Set 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 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 и показывать всю архитектуру вашего приложения.