From 25b1dabf73f1d24034b1bce4c8fbcca7d663f14e Mon Sep 17 00:00:00 2001 From: grigo Date: Wed, 3 Sep 2025 15:40:02 +0300 Subject: [PATCH] Created ship vectors (not added yet) Created menu Created udp support Created DockWidgets for compass and SOG/COG --- app/src/main/AndroidManifest.xml | 6 + .../com/grigowashere/aismap/MainActivity.java | 296 ++++- .../grigowashere/aismap/SettingsActivity.java | 323 +++++ .../aismap/controllers/AppController.java | 172 ++- .../aismap/controllers/GPSHybridTest.java | 184 --- .../aismap/controllers/NMEAParser.java | 233 +++- .../aismap/utils/SettingsManager.java | 202 +++ .../aismap/view/BaseDockWidget.java | 127 +- .../grigowashere/aismap/view/CompassView.java | 6 +- .../aismap/view/CoordinatesDockWidget.java | 275 ++++ app/src/main/res/layout/activity_main.xml | 39 +- app/src/main/res/layout/activity_settings.xml | 238 ++++ rawAssets/AisShipAssets.ai | 1157 +++++++++++++++++ rawAssets/AisShipAssets.png | Bin 0 -> 5747 bytes rawAssets/PNG/ChosenTargethdpi.png | Bin 0 -> 1530 bytes rawAssets/PNG/ChosenTargetldpi.png | Bin 0 -> 765 bytes rawAssets/PNG/ChosenTargetmdpi.png | Bin 0 -> 974 bytes rawAssets/PNG/ChosenTargetxhdpi.png | Bin 0 -> 2058 bytes rawAssets/PNG/ChosenTargetxxhdpi.png | Bin 0 -> 3759 bytes rawAssets/PNG/ChosenTargetxxxhdpi.png | Bin 0 -> 5635 bytes rawAssets/PNG/LosingTargethdpi.png | Bin 0 -> 3578 bytes rawAssets/PNG/LosingTargetldpi.png | Bin 0 -> 1762 bytes rawAssets/PNG/LosingTargetmdpi.png | Bin 0 -> 2363 bytes rawAssets/PNG/LosingTargetxhdpi.png | Bin 0 -> 4941 bytes rawAssets/PNG/LosingTargetxxhdpi.png | Bin 0 -> 8015 bytes rawAssets/PNG/LosingTargetxxxhdpi.png | Bin 0 -> 11668 bytes rawAssets/PNG/ScaleTargethdpi.png | Bin 0 -> 3058 bytes rawAssets/PNG/ScaleTargetldpi.png | Bin 0 -> 1524 bytes rawAssets/PNG/ScaleTargetmdpi.png | Bin 0 -> 2002 bytes rawAssets/PNG/ScaleTargetxhdpi.png | Bin 0 -> 4040 bytes rawAssets/PNG/ScaleTargetxxhdpi.png | Bin 0 -> 6158 bytes rawAssets/PNG/ScaleTargetxxxhdpi.png | Bin 0 -> 13184 bytes rawAssets/PNG/Targethdpi.png | Bin 0 -> 3010 bytes rawAssets/PNG/Targetldpi.png | Bin 0 -> 1470 bytes rawAssets/PNG/Targetmdpi.png | Bin 0 -> 2078 bytes rawAssets/PNG/Targetxhdpi.png | Bin 0 -> 4117 bytes rawAssets/PNG/Targetxxhdpi.png | Bin 0 -> 6745 bytes rawAssets/PNG/Targetxxxhdpi.png | Bin 0 -> 11515 bytes rawAssets/SVG/ChosenTarget.svg | 71 + rawAssets/SVG/LosingTarget.svg | 7 + rawAssets/SVG/ScaleTarget.svg | 6 + rawAssets/SVG/Target.svg | 6 + .../chosenTarget/PNG/ChosenTargethdpi.png | Bin 0 -> 1530 bytes .../chosenTarget/PNG/ChosenTargetldpi.png | Bin 0 -> 765 bytes .../chosenTarget/PNG/ChosenTargetmdpi.png | Bin 0 -> 974 bytes .../chosenTarget/PNG/ChosenTargetxhdpi.png | Bin 0 -> 2058 bytes .../chosenTarget/PNG/ChosenTargetxxhdpi.png | Bin 0 -> 3759 bytes .../chosenTarget/PNG/ChosenTargetxxxhdpi.png | Bin 0 -> 5635 bytes .../chosenTarget/PNG/LosingTargethdpi.png | Bin 0 -> 3578 bytes .../chosenTarget/PNG/LosingTargetldpi.png | Bin 0 -> 1762 bytes .../chosenTarget/PNG/LosingTargetmdpi.png | Bin 0 -> 2363 bytes .../chosenTarget/PNG/LosingTargetxhdpi.png | Bin 0 -> 4941 bytes .../chosenTarget/PNG/LosingTargetxxhdpi.png | Bin 0 -> 8015 bytes .../chosenTarget/PNG/LosingTargetxxxhdpi.png | Bin 0 -> 11668 bytes .../chosenTarget/PNG/ScaleTargethdpi.png | Bin 0 -> 3058 bytes .../chosenTarget/PNG/ScaleTargetldpi.png | Bin 0 -> 1524 bytes .../chosenTarget/PNG/ScaleTargetmdpi.png | Bin 0 -> 2002 bytes .../chosenTarget/PNG/ScaleTargetxhdpi.png | Bin 0 -> 4040 bytes .../chosenTarget/PNG/ScaleTargetxxhdpi.png | Bin 0 -> 6158 bytes .../chosenTarget/PNG/ScaleTargetxxxhdpi.png | Bin 0 -> 13184 bytes rawAssets/chosenTarget/PNG/Targethdpi.png | Bin 0 -> 3010 bytes rawAssets/chosenTarget/PNG/Targetldpi.png | Bin 0 -> 1470 bytes rawAssets/chosenTarget/PNG/Targetmdpi.png | Bin 0 -> 2078 bytes rawAssets/chosenTarget/PNG/Targetxhdpi.png | Bin 0 -> 4117 bytes rawAssets/chosenTarget/PNG/Targetxxhdpi.png | Bin 0 -> 6745 bytes rawAssets/chosenTarget/PNG/Targetxxxhdpi.png | Bin 0 -> 11515 bytes rawAssets/chosenTarget/SVG/ChosenTarget.svg | 71 + rawAssets/chosenTarget/SVG/LosingTarget.svg | 7 + rawAssets/chosenTarget/SVG/ScaleTarget.svg | 6 + rawAssets/chosenTarget/SVG/Target.svg | 6 + 70 files changed, 3145 insertions(+), 293 deletions(-) create mode 100644 app/src/main/java/com/grigowashere/aismap/SettingsActivity.java delete mode 100644 app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java create mode 100644 app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java create mode 100644 app/src/main/java/com/grigowashere/aismap/view/CoordinatesDockWidget.java create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 rawAssets/AisShipAssets.ai create mode 100644 rawAssets/AisShipAssets.png create mode 100644 rawAssets/PNG/ChosenTargethdpi.png create mode 100644 rawAssets/PNG/ChosenTargetldpi.png create mode 100644 rawAssets/PNG/ChosenTargetmdpi.png create mode 100644 rawAssets/PNG/ChosenTargetxhdpi.png create mode 100644 rawAssets/PNG/ChosenTargetxxhdpi.png create mode 100644 rawAssets/PNG/ChosenTargetxxxhdpi.png create mode 100644 rawAssets/PNG/LosingTargethdpi.png create mode 100644 rawAssets/PNG/LosingTargetldpi.png create mode 100644 rawAssets/PNG/LosingTargetmdpi.png create mode 100644 rawAssets/PNG/LosingTargetxhdpi.png create mode 100644 rawAssets/PNG/LosingTargetxxhdpi.png create mode 100644 rawAssets/PNG/LosingTargetxxxhdpi.png create mode 100644 rawAssets/PNG/ScaleTargethdpi.png create mode 100644 rawAssets/PNG/ScaleTargetldpi.png create mode 100644 rawAssets/PNG/ScaleTargetmdpi.png create mode 100644 rawAssets/PNG/ScaleTargetxhdpi.png create mode 100644 rawAssets/PNG/ScaleTargetxxhdpi.png create mode 100644 rawAssets/PNG/ScaleTargetxxxhdpi.png create mode 100644 rawAssets/PNG/Targethdpi.png create mode 100644 rawAssets/PNG/Targetldpi.png create mode 100644 rawAssets/PNG/Targetmdpi.png create mode 100644 rawAssets/PNG/Targetxhdpi.png create mode 100644 rawAssets/PNG/Targetxxhdpi.png create mode 100644 rawAssets/PNG/Targetxxxhdpi.png create mode 100644 rawAssets/SVG/ChosenTarget.svg create mode 100644 rawAssets/SVG/LosingTarget.svg create mode 100644 rawAssets/SVG/ScaleTarget.svg create mode 100644 rawAssets/SVG/Target.svg create mode 100644 rawAssets/chosenTarget/PNG/ChosenTargethdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ChosenTargetldpi.png create mode 100644 rawAssets/chosenTarget/PNG/ChosenTargetmdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ChosenTargetxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ChosenTargetxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ChosenTargetxxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/LosingTargethdpi.png create mode 100644 rawAssets/chosenTarget/PNG/LosingTargetldpi.png create mode 100644 rawAssets/chosenTarget/PNG/LosingTargetmdpi.png create mode 100644 rawAssets/chosenTarget/PNG/LosingTargetxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/LosingTargetxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/LosingTargetxxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ScaleTargethdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ScaleTargetldpi.png create mode 100644 rawAssets/chosenTarget/PNG/ScaleTargetmdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ScaleTargetxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ScaleTargetxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/ScaleTargetxxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/Targethdpi.png create mode 100644 rawAssets/chosenTarget/PNG/Targetldpi.png create mode 100644 rawAssets/chosenTarget/PNG/Targetmdpi.png create mode 100644 rawAssets/chosenTarget/PNG/Targetxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/Targetxxhdpi.png create mode 100644 rawAssets/chosenTarget/PNG/Targetxxxhdpi.png create mode 100644 rawAssets/chosenTarget/SVG/ChosenTarget.svg create mode 100644 rawAssets/chosenTarget/SVG/LosingTarget.svg create mode 100644 rawAssets/chosenTarget/SVG/ScaleTarget.svg create mode 100644 rawAssets/chosenTarget/SVG/Target.svg diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 145b29e..816f8b8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -43,6 +43,12 @@ + + centerOnVessel()); - btnTestCompass.setOnClickListener(v -> testCompass()); + btnMapOrientation.setOnClickListener(v -> toggleMapOrientation()); + btnSettings.setOnClickListener(v -> showSettings()); // Кнопка для показа информации о судне // Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info); @@ -114,6 +130,18 @@ public class MainActivity extends AppCompatActivity { // Настраиваем слушатель изменения размера док-виджета compassView.setOnDockResizeListener(newHeight -> { Log.d(TAG, "Compass dock height changed to: " + newHeight); + // Обновляем позицию панели управления при любом изменении размера docked виджета + updateControlPanelPosition(); + }); + + // Настраиваем слушатель изменения состояния docked + compassView.setOnDockStateChangeListener((isDocked, isTop) -> { + Log.d(TAG, "Compass dock state changed: docked=" + isDocked + ", top=" + isTop); + + // Перепозиционируем все docked виджеты + BaseDockWidget.repositionAllDockedWidgets((ViewGroup) compassView.getParent()); + + updateControlPanelPosition(); }); //smt changed // Настраиваем магнитный компас @@ -144,6 +172,48 @@ public class MainActivity extends AppCompatActivity { // Принудительная отрисовка compassView.invalidate(); + + // Инициализируем начальную позицию панели управления + compassView.post(() -> { + updateControlPanelPosition(); + }); + } + + private void setupCoordinatesWidget() { + // Настраиваем слушатель изменения размера dock-виджета + coordinatesWidget.setOnDockResizeListener(newHeight -> { + Log.d(TAG, "Coordinates dock height changed to: " + newHeight); + // Обновляем позицию панели управления при любом изменении размера docked виджета + updateControlPanelPosition(); + }); + + // Настраиваем слушатель изменения состояния docked + coordinatesWidget.setOnDockStateChangeListener((isDocked, isTop) -> { + Log.d(TAG, "Coordinates dock state changed: docked=" + isDocked + ", top=" + isTop); + + // Перепозиционируем все docked виджеты + BaseDockWidget.repositionAllDockedWidgets((ViewGroup) coordinatesWidget.getParent()); + + updateControlPanelPosition(); + }); + + // Устанавливаем виджет координат в dock-режим внизу экрана + coordinatesWidget.post(() -> { + Log.d(TAG, "Setting coordinates widget to dock mode"); + coordinatesWidget.setDocked(true, false, 0, 0); // false = dock снизу + coordinatesWidget.invalidate(); // Принудительная отрисовка + + // Принудительно обновляем виджет с тестовыми данными + 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); + + updateControlPanelPosition(); + }); } private void onUpdateCompass(float azimuth, List nearbyVessels) { @@ -248,6 +318,9 @@ public class MainActivity extends AppCompatActivity { } private void initializeControllers() { + // Инициализация менеджера настроек + settingsManager = new SettingsManager(this); + // Инициализация главного контроллера appController = new AppController(this); @@ -292,10 +365,8 @@ public class MainActivity extends AppCompatActivity { } private void startControllers() { - // Включаем GPS и UDP по умолчанию - appController.setGPSLocationEnabled(true); - appController.setAndroidNMEAEnabled(true); - appController.setUDPEnabled(true); + // Загружаем настройки и применяем их + applySettings(); // Запускаем все слушатели appController.startAllListeners(); @@ -331,6 +402,11 @@ public class MainActivity extends AppCompatActivity { // tvStatus.setText("Статус: GPS активен, данные получены"); // } + // Обновляем виджет координат + if (coordinatesWidget != null) { + coordinatesWidget.updateVessel(vessel); + } + // Обновляем BottomSheet, если он открыт if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { updateBottomSheetUI(); @@ -387,43 +463,94 @@ public class MainActivity extends AppCompatActivity { Toast.makeText(this, "Карта центрирована на судне", Toast.LENGTH_SHORT).show(); } - private void testCompass() { - if (compassView != null) { - // Создаем тестовые AIS суда - List testVessels = new ArrayList<>(); - - AISVessel testVessel1 = new AISVessel("123456789"); - testVessel1.setLatitude(59.9343); - testVessel1.setLongitude(30.3351); - testVessel1.setCourse(45); - testVessel1.setSpeed(10); - testVessel1.setNavigationalStatus("under way using engine"); - testVessels.add(testVessel1); - - AISVessel testVessel2 = new AISVessel("987654321"); - testVessel2.setLatitude(59.9343); - testVessel2.setLongitude(30.3351); - testVessel2.setCourse(180); - testVessel2.setSpeed(5); - testVessel2.setNavigationalStatus("at anchor"); - testVessels.add(testVessel2); - - compassView.updateNearbyVessels(testVessels); - - // Проверяем доступность магнитного компаса - if (compassSensor.isAvailable()) { - Toast.makeText(this, "Магнитный компас доступен и работает", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(this, "Магнитный компас недоступен", Toast.LENGTH_SHORT).show(); - } + private void toggleMapOrientation() { + // TODO: Реализовать переключение ориентации карты + // Состояния: север, курс, компас + Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show(); + } + + private void showSettings() { + Intent intent = new Intent(this, SettingsActivity.class); + startActivityForResult(intent, SETTINGS_REQUEST_CODE); + } + + /** + * Обновляет позицию панели управления в зависимости от состояния docked виджетов + */ + private void updateControlPanelPosition() { + if (controlPanel != null) { + runOnUiThread(() -> { + // Получаем текущие параметры layout + android.widget.RelativeLayout.LayoutParams params = + (android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams(); + + int topMargin = dpToPx(16); // По умолчанию отступ сверху + int bottomMargin = dpToPx(16); // По умолчанию отступ снизу + + // Вычисляем общую высоту всех docked виджетов сверху + int totalTopHeight = 0; + if (compassView != null && compassView.isDocked() && compassView.isDockTop()) { + totalTopHeight += compassView.getHeight(); + Log.d(TAG, "Compass docked top, height: " + compassView.getHeight()); + } + if (coordinatesWidget != null && coordinatesWidget.isDocked() && coordinatesWidget.isDockTop()) { + totalTopHeight += coordinatesWidget.getHeight(); + Log.d(TAG, "Coordinates docked top, height: " + coordinatesWidget.getHeight()); + } + + // Вычисляем общую высоту всех docked виджетов снизу + int totalBottomHeight = 0; + if (compassView != null && compassView.isDocked() && !compassView.isDockTop()) { + totalBottomHeight += compassView.getHeight(); + Log.d(TAG, "Compass docked bottom, height: " + compassView.getHeight()); + } + if (coordinatesWidget != null && coordinatesWidget.isDocked() && !coordinatesWidget.isDockTop()) { + totalBottomHeight += coordinatesWidget.getHeight(); + Log.d(TAG, "Coordinates docked bottom, height: " + coordinatesWidget.getHeight()); + } + + // Устанавливаем отступы с учетом всех docked виджетов + if (totalTopHeight > 0) { + topMargin = totalTopHeight + dpToPx(8); // + небольшой отступ + } + if (totalBottomHeight > 0) { + bottomMargin = totalBottomHeight + dpToPx(8); // + небольшой отступ + } + + // Устанавливаем отступы + params.topMargin = topMargin; + params.bottomMargin = bottomMargin; + + // Применяем новые параметры + controlPanel.setLayoutParams(params); + + Log.d(TAG, "Control panel position updated: " + + "topMargin=" + topMargin + "px, " + + "bottomMargin=" + bottomMargin + "px, " + + "totalTopHeight=" + totalTopHeight + "px, " + + "totalBottomHeight=" + totalBottomHeight + "px, " + + "compassDocked=" + (compassView != null ? compassView.isDocked() : false) + + ", compassTop=" + (compassView != null ? compassView.isDockTop() : false) + + ", coordinatesDocked=" + (coordinatesWidget != null ? coordinatesWidget.isDocked() : false) + + ", coordinatesTop=" + (coordinatesWidget != null ? coordinatesWidget.isDockTop() : false)); + }); } } + + private void clearAIS() { appController.clearAISVessels(); Toast.makeText(this, "AIS суда очищены", Toast.LENGTH_SHORT).show(); } + /** + * Конвертирует dp в px + */ + private int dpToPx(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + @Override protected void onStart() { super.onStart(); @@ -548,6 +675,32 @@ public class MainActivity extends AppCompatActivity { } } + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == SETTINGS_REQUEST_CODE) { + if (resultCode == RESULT_OK && data != null) { + boolean settingsChanged = data.getBooleanExtra("settings_changed", false); + boolean needsRestart = data.getBooleanExtra("needs_restart", false); + + if (settingsChanged) { + Log.i(TAG, "Настройки изменены, применяем изменения"); + + if (needsRestart) { + Log.i(TAG, "Требуется перезапуск сервисов"); + restartServices(); + } else { + Log.i(TAG, "Применяем настройки без перезапуска"); + applySettings(); + } + + Toast.makeText(this, "Настройки применены", Toast.LENGTH_SHORT).show(); + } + } + } + } + // Меню @Override @@ -1082,4 +1235,75 @@ public class MainActivity extends AppCompatActivity { } } } + + /** + * Применяет настройки к контроллерам + */ + private void applySettings() { + if (settingsManager == null || appController == null) { + Log.w(TAG, "SettingsManager или AppController не инициализированы"); + return; + } + + try { + // Применяем UDP настройки + int udpPort = settingsManager.getUDPPort(); + boolean udpEnabled = settingsManager.isUDPEnabled(); + + appController.setUDPPort(udpPort); + appController.setUDPEnabled(udpEnabled); + + // Применяем NMEA настройки + boolean androidNMEAEnabled = settingsManager.isAndroidNMEAEnabled(); + boolean udpNMEAEnabled = settingsManager.isUDPNMEAEnabled(); + + appController.setAndroidNMEAEnabled(androidNMEAEnabled); + appController.setUDPNMEAEnabled(udpNMEAEnabled); + + // Применяем режим данных + String dataMode = settingsManager.getDataMode(); + appController.setDataMode(dataMode); + + Log.i(TAG, "Настройки применены: " + settingsManager.getSettingsSummary()); + + } catch (Exception e) { + Log.e(TAG, "Ошибка при применении настроек: " + e.getMessage(), e); + Toast.makeText(this, "Ошибка при применении настроек", Toast.LENGTH_SHORT).show(); + } + } + + /** + * Перезапускает сервисы с новыми настройками + */ + private void restartServices() { + if (appController == null) { + Log.w(TAG, "AppController не инициализирован"); + return; + } + + try { + Log.i(TAG, "Перезапускаем сервисы..."); + + // Останавливаем все слушатели + appController.stopAllListeners(); + + // Применяем новые настройки + applySettings(); + + // Перезапускаем UDP слушатель с новым портом, если нужно + if (settingsManager.shouldRestartUDP(appController.getUDPPort(), appController.isUDPEnabled())) { + appController.restartUDPListener(); + } + + // Запускаем слушатели с новыми настройками + appController.startAllListeners(); + + Log.i(TAG, "Сервисы успешно перезапущены"); + Log.i(TAG, "Статус настроек: " + appController.getSettingsStatus()); + + } catch (Exception e) { + Log.e(TAG, "Ошибка при перезапуске сервисов: " + e.getMessage(), e); + Toast.makeText(this, "Ошибка при перезапуске сервисов", Toast.LENGTH_SHORT).show(); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java b/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java new file mode 100644 index 0000000..fcccbdf --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/SettingsActivity.java @@ -0,0 +1,323 @@ +package com.grigowashere.aismap; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.Button; +import android.widget.EditText; +import android.widget.RadioButton; +import android.widget.RadioGroup; +import android.widget.Toast; + +import com.google.android.material.switchmaterial.SwitchMaterial; + +import androidx.appcompat.app.AppCompatActivity; + +import com.grigowashere.aismap.utils.SettingsManager; + +/** + * Экран настроек приложения + */ +public class SettingsActivity extends AppCompatActivity { + + private static final String TAG = "SettingsActivity"; + + private SettingsManager settingsManager; + + // UI элементы + private EditText etUDPPort; + private SwitchMaterial switchUDPEnabled; + private SwitchMaterial switchAndroidNMEAEnabled; + private SwitchMaterial switchUDPNMEAEnabled; + private RadioGroup radioGroupDataMode; + private RadioButton radioHybridMode; + private RadioButton radioNMEAOnly; + private RadioButton radioAndroidOnly; + private Button btnCancel; + private Button btnSave; + + // Состояние настроек до изменений + private int originalUDPPort; + private boolean originalUDPEnabled; + private boolean originalAndroidNMEAEnabled; + private boolean originalUDPNMEAEnabled; + private String originalDataMode; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + + // Инициализируем менеджер настроек + settingsManager = new SettingsManager(this); + + // Инициализируем UI элементы + initializeViews(); + + // Загружаем текущие настройки + loadCurrentSettings(); + + // Сохраняем оригинальные значения + saveOriginalSettings(); + + // Настраиваем обработчики событий + setupEventHandlers(); + + Log.i(TAG, "SettingsActivity создан"); + } + + /** + * Инициализирует UI элементы + */ + private void initializeViews() { + etUDPPort = findViewById(R.id.et_udp_port); + switchUDPEnabled = findViewById(R.id.switch_udp_enabled); + switchAndroidNMEAEnabled = findViewById(R.id.switch_android_nmea_enabled); + switchUDPNMEAEnabled = findViewById(R.id.switch_udp_nmea_enabled); + radioGroupDataMode = findViewById(R.id.radio_group_data_mode); + radioHybridMode = findViewById(R.id.radio_hybrid_mode); + radioNMEAOnly = findViewById(R.id.radio_nmea_only); + radioAndroidOnly = findViewById(R.id.radio_android_only); + btnCancel = findViewById(R.id.btn_cancel); + btnSave = findViewById(R.id.btn_save); + } + + /** + * Загружает текущие настройки в UI + */ + private void loadCurrentSettings() { + // UDP настройки + etUDPPort.setText(String.valueOf(settingsManager.getUDPPort())); + switchUDPEnabled.setChecked(settingsManager.isUDPEnabled()); + + // NMEA настройки + switchAndroidNMEAEnabled.setChecked(settingsManager.isAndroidNMEAEnabled()); + switchUDPNMEAEnabled.setChecked(settingsManager.isUDPNMEAEnabled()); + + // Режим данных + String dataMode = settingsManager.getDataMode(); + switch (dataMode) { + case SettingsManager.DATA_MODE_HYBRID: + radioHybridMode.setChecked(true); + break; + case SettingsManager.DATA_MODE_NMEA_ONLY: + radioNMEAOnly.setChecked(true); + break; + case SettingsManager.DATA_MODE_ANDROID_ONLY: + radioAndroidOnly.setChecked(true); + break; + } + + Log.i(TAG, "Настройки загружены в UI"); + } + + /** + * Сохраняет оригинальные значения настроек + */ + private void saveOriginalSettings() { + originalUDPPort = settingsManager.getUDPPort(); + originalUDPEnabled = settingsManager.isUDPEnabled(); + originalAndroidNMEAEnabled = settingsManager.isAndroidNMEAEnabled(); + originalUDPNMEAEnabled = settingsManager.isUDPNMEAEnabled(); + originalDataMode = settingsManager.getDataMode(); + + Log.i(TAG, "Оригинальные настройки сохранены"); + } + + /** + * Настраивает обработчики событий + */ + private void setupEventHandlers() { + // Кнопка отмены + btnCancel.setOnClickListener(v -> { + Log.i(TAG, "Нажата кнопка отмены"); + finish(); + }); + + // Кнопка сохранения + btnSave.setOnClickListener(v -> { + Log.i(TAG, "Нажата кнопка сохранения"); + saveSettings(); + }); + + // Обработчик изменения режима данных + radioGroupDataMode.setOnCheckedChangeListener((group, checkedId) -> { + updateDataModeDescription(); + }); + + // Обработчики переключателей для валидации + switchAndroidNMEAEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> { + validateDataModeSettings(); + }); + + switchUDPNMEAEnabled.setOnCheckedChangeListener((buttonView, isChecked) -> { + validateDataModeSettings(); + }); + } + + /** + * Обновляет описание режима данных + */ + private void updateDataModeDescription() { + // Здесь можно добавить динамическое обновление описаний + // в зависимости от выбранного режима + } + + /** + * Валидирует настройки режима данных + */ + private void validateDataModeSettings() { + boolean androidNMEA = switchAndroidNMEAEnabled.isChecked(); + boolean udpNMEA = switchUDPNMEAEnabled.isChecked(); + + // Если оба источника отключены, показываем предупреждение + if (!androidNMEA && !udpNMEA) { + Toast.makeText(this, "Внимание: Все источники данных отключены!", + Toast.LENGTH_LONG).show(); + } + + // Если выбран режим "только NMEA", но UDP NMEA отключен + if (radioNMEAOnly.isChecked() && !udpNMEA) { + Toast.makeText(this, "Для режима 'Только NMEA' необходимо включить UDP NMEA", + Toast.LENGTH_LONG).show(); + } + + // Если выбран режим "только Android", но Android NMEA отключен + if (radioAndroidOnly.isChecked() && !androidNMEA) { + Toast.makeText(this, "Для режима 'Только Android GPS' необходимо включить Android NMEA", + Toast.LENGTH_LONG).show(); + } + } + + /** + * Сохраняет настройки + */ + private void saveSettings() { + try { + // Валидируем UDP порт + String portText = etUDPPort.getText().toString().trim(); + if (portText.isEmpty()) { + Toast.makeText(this, "Порт не может быть пустым", Toast.LENGTH_SHORT).show(); + return; + } + + int udpPort; + try { + udpPort = Integer.parseInt(portText); + if (udpPort < 1 || udpPort > 65535) { + Toast.makeText(this, "Порт должен быть от 1 до 65535", Toast.LENGTH_SHORT).show(); + return; + } + } catch (NumberFormatException e) { + Toast.makeText(this, "Некорректный формат порта", Toast.LENGTH_SHORT).show(); + return; + } + + // Получаем выбранный режим данных + String dataMode = getSelectedDataMode(); + + // Валидируем режим данных + if (!validateDataMode(dataMode)) { + return; + } + + // Сохраняем настройки + settingsManager.setUDPPort(udpPort); + settingsManager.setUDPEnabled(switchUDPEnabled.isChecked()); + settingsManager.setAndroidNMEAEnabled(switchAndroidNMEAEnabled.isChecked()); + settingsManager.setUDPNMEAEnabled(switchUDPNMEAEnabled.isChecked()); + settingsManager.setDataMode(dataMode); + + Log.i(TAG, "Настройки сохранены: " + settingsManager.getSettingsSummary()); + + // Проверяем, нужно ли уведомить MainActivity об изменениях + boolean needsRestart = checkIfRestartNeeded(); + + // Возвращаем результат + Intent resultIntent = new Intent(); + resultIntent.putExtra("settings_changed", true); + resultIntent.putExtra("needs_restart", needsRestart); + resultIntent.putExtra("udp_port", udpPort); + resultIntent.putExtra("udp_enabled", switchUDPEnabled.isChecked()); + resultIntent.putExtra("android_nmea_enabled", switchAndroidNMEAEnabled.isChecked()); + resultIntent.putExtra("udp_nmea_enabled", switchUDPNMEAEnabled.isChecked()); + resultIntent.putExtra("data_mode", dataMode); + + setResult(RESULT_OK, resultIntent); + + Toast.makeText(this, "Настройки сохранены", Toast.LENGTH_SHORT).show(); + finish(); + + } catch (Exception e) { + Log.e(TAG, "Ошибка при сохранении настроек: " + e.getMessage(), e); + Toast.makeText(this, "Ошибка при сохранении настроек", Toast.LENGTH_SHORT).show(); + } + } + + /** + * Получает выбранный режим данных + */ + private String getSelectedDataMode() { + int checkedId = radioGroupDataMode.getCheckedRadioButtonId(); + + if (checkedId == R.id.radio_hybrid_mode) { + return SettingsManager.DATA_MODE_HYBRID; + } else if (checkedId == R.id.radio_nmea_only) { + return SettingsManager.DATA_MODE_NMEA_ONLY; + } else if (checkedId == R.id.radio_android_only) { + return SettingsManager.DATA_MODE_ANDROID_ONLY; + } else { + return SettingsManager.DATA_MODE_HYBRID; // По умолчанию + } + } + + /** + * Валидирует режим данных + */ + private boolean validateDataMode(String dataMode) { + boolean androidNMEA = switchAndroidNMEAEnabled.isChecked(); + boolean udpNMEA = switchUDPNMEAEnabled.isChecked(); + + switch (dataMode) { + case SettingsManager.DATA_MODE_HYBRID: + if (!androidNMEA && !udpNMEA) { + Toast.makeText(this, "Для гибридного режима необходимо включить хотя бы один источник данных", + Toast.LENGTH_LONG).show(); + return false; + } + break; + + case SettingsManager.DATA_MODE_NMEA_ONLY: + if (!udpNMEA) { + Toast.makeText(this, "Для режима 'Только NMEA' необходимо включить UDP NMEA", + Toast.LENGTH_LONG).show(); + return false; + } + break; + + case SettingsManager.DATA_MODE_ANDROID_ONLY: + if (!androidNMEA) { + Toast.makeText(this, "Для режима 'Только Android GPS' необходимо включить Android NMEA", + Toast.LENGTH_LONG).show(); + return false; + } + break; + } + + return true; + } + + /** + * Проверяет, нужно ли перезапустить сервисы + */ + private boolean checkIfRestartNeeded() { + return settingsManager.shouldRestartUDP(originalUDPPort, originalUDPEnabled) || + settingsManager.shouldRestartNMEA(originalAndroidNMEAEnabled, originalUDPNMEAEnabled, originalDataMode); + } + + @Override + public void onBackPressed() { + Log.i(TAG, "Нажата кнопка назад"); + finish(); + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java b/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java index aaa5b74..c1debdc 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/AppController.java @@ -37,7 +37,10 @@ public class AppController implements private boolean isUDPEnabled; private boolean isAndroidNMEAEnabled; + private boolean isUDPNMEAEnabled; private boolean isGPSLocationEnabled; + private int udpPort; + private String dataMode; // Callback для обновления UI private UIUpdateCallback uiUpdateCallback; @@ -82,7 +85,8 @@ public class AppController implements nmeaParser.setHybridMode(true); // Инициализация UDP слушателя (порт 10110 - стандартный для AIS) - udpListener = new UDPListener(10110); + udpPort = 10110; + udpListener = new UDPListener(udpPort); udpListener.setCallback(this); // Инициализация Android NMEA слушателя (для курса, скорости, DOP) @@ -131,29 +135,10 @@ public class AppController implements }); } - // Тестируем NMEA парсер (временно) - testNMEAParser(); + } - /** - * Тестирует NMEA парсер (временно для отладки) - */ - private void testNMEAParser() { - Log.i(TAG, "Тестируем NMEA парсер..."); - - // Тестовые NMEA сообщения - String[] testMessages = { - "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47", - "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A", - "$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48", - "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.8,1.0*3E" - }; - - for (String message : testMessages) { - Log.i(TAG, "Тестируем сообщение: " + message); - nmeaParser.parseNMEA(message); - } - } + /** * Останавливает все слушатели @@ -221,12 +206,7 @@ public class AppController implements } } - /** - * Устанавливает UDP порт - */ - public void setUDPPort(int port) { - udpListener.setPort(port); - } + /** * Отправляет данные по UDP @@ -328,7 +308,20 @@ public class AppController implements @Override public void onVesselUpdated(Vessel vessel) { - // В гибридном режиме обновляем только дополнительные данные + Log.i(TAG, "🔄 onVesselUpdated вызван: lat=" + vessel.getLatitude() + + ", lon=" + vessel.getLongitude() + + ", course=" + vessel.getCourse() + + ", speed=" + vessel.getSpeed()); + + // Обновляем координаты, если они есть (для режима "только NMEA") + if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { + ownVessel.setLatitude(vessel.getLatitude()); + ownVessel.setLongitude(vessel.getLongitude()); + Log.i(TAG, "📍 Координаты обновлены из NMEA: lat=" + vessel.getLatitude() + + ", lon=" + vessel.getLongitude()); + } + + // Обновляем дополнительные данные if (vessel.getCourse() > 0) { ownVessel.setCourse(vessel.getCourse()); updateCompass(); // Обновляем компас при изменении курса @@ -347,6 +340,20 @@ public class AppController implements ", speed=" + vessel.getSpeed() + ", satellites=" + vessel.getSatellites()); + // Обновляем карту в главном потоке + if (mapInterface != null) { + Log.i(TAG, "Обновляем позицию на карте из NMEA..."); + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + try { + Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition из NMEA..."); + mapInterface.updateOwnVesselPosition(ownVessel); + Log.i(TAG, "Позиция на карте обновлена из NMEA"); + } catch (Exception e) { + Log.e(TAG, "Ошибка обновления позиции на карте из NMEA: " + e.getMessage(), e); + } + }); + } + // Обновляем UI if (uiUpdateCallback != null) { uiUpdateCallback.onVesselPositionUpdated(ownVessel); @@ -592,4 +599,111 @@ public class AppController implements executor.shutdown(); } } + + // Методы для управления настройками + + /** + * Устанавливает UDP порт + */ + public void setUDPPort(int port) { + if (port < 1 || port > 65535) { + Log.w(TAG, "Некорректный UDP порт: " + port); + return; + } + + this.udpPort = port; + Log.i(TAG, "UDP порт установлен: " + port); + + // Если UDP слушатель уже создан, нужно будет его пересоздать + if (udpListener != null && udpListener.getPort() != port) { + Log.i(TAG, "UDP порт изменен, потребуется перезапуск UDP слушателя"); + } + } + + /** + * Получает текущий UDP порт + */ + public int getUDPPort() { + return udpPort; + } + + /** + * Включает/выключает UDP NMEA + */ + public void setUDPNMEAEnabled(boolean enabled) { + this.isUDPNMEAEnabled = enabled; + Log.i(TAG, "UDP NMEA: " + (enabled ? "включен" : "выключен")); + } + + /** + * Проверяет, включен ли UDP NMEA + */ + public boolean isUDPNMEAEnabled() { + return isUDPNMEAEnabled; + } + + /** + * Устанавливает режим работы с данными + */ + public void setDataMode(String mode) { + this.dataMode = mode; + Log.i(TAG, "🔄 Режим данных установлен: " + mode); + + // Применяем режим к NMEA парсеру + if (nmeaParser != null) { + boolean hybridMode = "hybrid".equals(mode); + nmeaParser.setHybridMode(hybridMode); + Log.i(TAG, "📍 Гибридный режим NMEA парсера: " + hybridMode); + Log.i(TAG, "📍 В режиме '" + mode + "' координаты будут " + + (hybridMode ? "браться из Android GPS API" : "браться из NMEA сообщений")); + } + } + + /** + * Получает текущий режим работы с данными + */ + public String getDataMode() { + return dataMode; + } + + /** + * Перезапускает UDP слушатель с новым портом + */ + public void restartUDPListener() { + if (udpListener != null) { + Log.i(TAG, "Перезапускаем UDP слушатель с портом: " + udpPort); + + // Останавливаем текущий слушатель + udpListener.stop(); + udpListener.cleanup(); + + // Создаем новый слушатель с новым портом + udpListener = new UDPListener(udpPort); + udpListener.setCallback(this); + + // Запускаем, если UDP включен + if (isUDPEnabled) { + udpListener.start(); + Log.i(TAG, "UDP слушатель перезапущен на порту: " + udpPort); + } + } + } + + /** + * Получает статус всех настроек + */ + public String getSettingsStatus() { + return String.format( + "UDP: порт=%d, включен=%s, NMEA=%s\n" + + "Android NMEA: %s\n" + + "GPS Location: %s\n" + + "Режим данных: %s", + udpPort, + isUDPEnabled ? "да" : "нет", + isUDPNMEAEnabled ? "включен" : "выключен", + isAndroidNMEAEnabled ? "включен" : "выключен", + isGPSLocationEnabled ? "включен" : "выключен", + dataMode != null ? dataMode : "не установлен" + ); + } } diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java b/app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java deleted file mode 100644 index 1d1d3c6..0000000 --- a/app/src/main/java/com/grigowashere/aismap/controllers/GPSHybridTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package com.grigowashere.aismap.controllers; - -import android.content.Context; -import android.util.Log; -import com.grigowashere.aismap.models.Vessel; - -/** - * Тестовый класс для демонстрации работы гибридного GPS подхода - * Показывает, как координаты получаются через Location API, а остальное через NMEA - */ -public class GPSHybridTest { - - private static final String TAG = "GPSHybridTest"; - - private Context context; - private GPSLocationListener gpsLocationListener; - private NMEAParser nmeaParser; - private Vessel testVessel; - - public GPSHybridTest(Context context) { - this.context = context; - this.testVessel = new Vessel(); - - // Инициализируем компоненты - gpsLocationListener = new GPSLocationListener(context); - nmeaParser = new NMEAParser(); - - // Связываем их для гибридного режима - nmeaParser.setGPSLocationListener(gpsLocationListener); - nmeaParser.setHybridMode(true); - - // Устанавливаем callback'и - gpsLocationListener.setCallback(new GPSLocationListener.LocationCallback() { - @Override - public void onLocationUpdated(Vessel vessel) { - Log.i(TAG, "📍 GPS Location получен: " + vessel.getLatitude() + ", " + vessel.getLongitude()); - Log.i(TAG, "📍 Точность: " + vessel.getAccuracy() + "м"); - - // Обновляем координаты - testVessel.setLatitude(vessel.getLatitude()); - testVessel.setLongitude(vessel.getLongitude()); - testVessel.setAccuracy(vessel.getAccuracy()); - testVessel.setFixTime(vessel.getFixTime()); - testVessel.setFixQuality(vessel.getFixQuality()); - - logVesselStatus(); - } - - @Override - public void onGPSStatusChanged(int status) { - Log.i(TAG, "GPS статус: " + status); - } - - @Override - public void onError(String error) { - Log.e(TAG, "GPS Location ошибка: " + error); - } - }); - - nmeaParser.setListener(new NMEAParser.NMEAParserListener() { - @Override - public void onVesselUpdated(Vessel vessel) { - Log.i(TAG, "📡 NMEA данные получены: course=" + vessel.getCourse() + - ", speed=" + vessel.getSpeed() + - ", satellites=" + vessel.getSatellites()); - - // Обновляем дополнительные данные - if (vessel.getCourse() > 0) testVessel.setCourse(vessel.getCourse()); - if (vessel.getSpeed() > 0) testVessel.setSpeed(vessel.getSpeed()); - if (vessel.getSatellites() > 0) testVessel.setSatellites(vessel.getSatellites()); - if (vessel.getAltitude() != 0) testVessel.setAltitude(vessel.getAltitude()); - - logVesselStatus(); - } - - @Override - public void onAISVesselUpdated(com.grigowashere.aismap.models.AISVessel vessel) { - Log.i(TAG, "🚢 AIS судно: " + vessel); - } - - @Override - public void onParseError(String error) { - Log.e(TAG, "NMEA ошибка: " + error); - } - - @Override - public void onDOPUpdated(double pdop, double hdop, double vdop) { - Log.i(TAG, "📊 DOP: PDOP=" + pdop + ", HDOP=" + hdop + ", VDOP=" + vdop); - - testVessel.setPdop(pdop); - testVessel.setHdop(hdop); - testVessel.setVdop(vdop); - - logVesselStatus(); - } - }); - } - - /** - * Запускает тест - */ - public void startTest() { - Log.i(TAG, "🚀 Запускаем тест гибридного GPS подхода..."); - - // Запускаем GPS Location Listener - boolean gpsSuccess = gpsLocationListener.startListening(); - if (gpsSuccess) { - Log.i(TAG, "✅ GPS Location Listener запущен"); - } else { - Log.e(TAG, "❌ Не удалось запустить GPS Location Listener"); - } - - // Тестируем NMEA парсер с тестовыми сообщениями - testNMEAParser(); - } - - /** - * Тестирует NMEA парсер - */ - private void testNMEAParser() { - Log.i(TAG, "🧪 Тестируем NMEA парсер..."); - - // Тестовые NMEA сообщения - String[] testMessages = { - // GGA - количество спутников и высота - "$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47", - - // RMC - курс и скорость - "$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A", - - // VTG - курс и скорость (альтернативный источник) - "$GPVTG,054.7,T,034.4,M,005.5,N,010.2,K*48", - - // GSA - DOP и активные спутники - "$GPGSA,A,3,01,02,03,04,05,06,07,08,09,10,11,12,1.2,0.8,1.0*3E", - - // GSV - спутники в поле зрения - "$GPGSV,3,1,12,01,05,040,3,02,46,000,4,03,42,350,4,04,42,000,4*7F" - }; - - for (String message : testMessages) { - Log.i(TAG, "📡 Тестируем NMEA: " + message); - nmeaParser.parseNMEA(message); - } - } - - /** - * Логирует текущий статус судна - */ - private void logVesselStatus() { - Log.i(TAG, "🚢 === СТАТУС СУДНА ==="); - Log.i(TAG, "📍 Координаты: " + testVessel.getLatitude() + ", " + testVessel.getLongitude()); - Log.i(TAG, "🎯 Точность: " + testVessel.getAccuracy() + "м (" + testVessel.getGPSQualityDescription() + ")"); - Log.i(TAG, "🧭 Курс: " + testVessel.getCourse() + "°"); - Log.i(TAG, "⚡ Скорость: " + testVessel.getSpeed() + " узлов"); - Log.i(TAG, "Спутники: " + testVessel.getSatellites() + "/" + testVessel.getActiveSatellites()); - Log.i(TAG, "📊 DOP: PDOP=" + testVessel.getPdop() + ", HDOP=" + testVessel.getHdop() + ", VDOP=" + testVessel.getVdop()); - Log.i(TAG, "🏔️ Высота: " + testVessel.getAltitude() + "м"); - Log.i(TAG, "🔧 Качество фикса: " + testVessel.getFixQuality()); - Log.i(TAG, "=========================="); - } - - /** - * Останавливает тест - */ - public void stopTest() { - Log.i(TAG, "⏹️ Останавливаем тест..."); - - if (gpsLocationListener != null) { - gpsLocationListener.stopListening(); - } - - Log.i(TAG, "✅ Тест остановлен"); - } - - /** - * Освобождает ресурсы - */ - public void cleanup() { - if (gpsLocationListener != null) { - gpsLocationListener.cleanup(); - } - } -} 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 6afa31d..58c23d5 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java @@ -23,7 +23,7 @@ public class NMEAParser { ); private static final Pattern RMC_PATTERN = Pattern.compile( - "\\$G[PN]RMC,(\\d{6}\\.\\d{2}),([AV]),(\\d{4}\\.\\d+),([NS]),(\\d{5}\\.\\d+),([EW]),([^,]*),([^,]*),(\\d{6}),([^,]*),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" + "\\$G[PN]RMC,(\\d{6}\\.\\d{2}),([AV][^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),(\\d{6}),([^,]*),([^,]*),([^,]*),?([^,]*)?\\*([0-9A-F]{2})" ); private static final Pattern VTG_PATTERN = Pattern.compile( @@ -44,7 +44,17 @@ public class NMEAParser { // Паттерн для GSA сообщения (DOP и активные спутники) private static final Pattern GSA_PATTERN = Pattern.compile( - "\\$G[PN]GSA,([AM]),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),(\\d+),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" + "\\$G[PN]GSA,([AM]),(\\d+),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" + ); + + // Паттерн для обрезанных GSA сообщений + private static final Pattern GSA_TRUNCATED_PATTERN = Pattern.compile( + "\\$G[PN]GSA,([^,]*),([^,]*),([^,]*)\\*([0-9A-F]{2})" + ); + + // Паттерн для ZDA сообщения (Date and Time) + private static final Pattern ZDA_PATTERN = Pattern.compile( + "\\$G[PN]ZDA,(\\d{6}\\.\\d{2}),(\\d{2}),(\\d{2}),(\\d{4}),(\\d{2}),(\\d{2})\\*([0-9A-F]{2})" ); private static final Pattern AIS_PATTERN = Pattern.compile( @@ -97,7 +107,9 @@ public class NMEAParser { */ public void setHybridMode(boolean enabled) { this.hybridMode = enabled; - Log.i(TAG, "Гибридный режим: " + (enabled ? "включен" : "отключен")); + Log.i(TAG, "🔄 Гибридный режим: " + (enabled ? "включен" : "отключен")); + Log.i(TAG, "📍 В режиме " + (enabled ? "гибридном" : "только NMEA") + " координаты будут " + + (enabled ? "браться из Android GPS API" : "браться из NMEA сообщений")); } /** @@ -110,6 +122,10 @@ public class NMEAParser { // Очищаем сообщение от лишних символов String cleanedSentence = cleanNMEASentence(nmeaSentence); + if (cleanedSentence == null) { + Log.w(TAG, "NMEA сообщение не удалось очистить или слишком короткое: " + nmeaSentence); + return; + } Log.d(TAG, "Парсим NMEA: " + cleanedSentence); try { @@ -121,12 +137,14 @@ public class NMEAParser { parseVTG(cleanedSentence); } else if (cleanedSentence.startsWith("$GPGLL") || cleanedSentence.startsWith("$GNGLL")) { parseGLL(cleanedSentence); - } else if (cleanedSentence.startsWith("$GPGSV") || cleanedSentence.startsWith("$GAGSV") || cleanedSentence.startsWith("$GLGSV") || cleanedSentence.startsWith("$GNGSA")) { + } else if (cleanedSentence.startsWith("$GPGSV") || cleanedSentence.startsWith("$GAGSV") || cleanedSentence.startsWith("$GLGSV") || cleanedSentence.startsWith("$GBGSV") || cleanedSentence.startsWith("$GNGSA")) { parseGSV(cleanedSentence); } else if (cleanedSentence.startsWith("$GNGNS")) { parseGNS(cleanedSentence); } else if (cleanedSentence.startsWith("$GPGSA") || cleanedSentence.startsWith("$GNGSA")) { parseGSA(cleanedSentence); + } else if (cleanedSentence.startsWith("$GPZDA") || cleanedSentence.startsWith("$GNZDA")) { + parseZDA(cleanedSentence); } else if (cleanedSentence.startsWith("!AIVDM")) { parseAIS(cleanedSentence); } else { @@ -144,17 +162,57 @@ public class NMEAParser { * Очищает NMEA сообщение от лишних символов */ private String cleanNMEASentence(String sentence) { - if (sentence == null) { + if (sentence == null || sentence.trim().isEmpty()) { return null; } // Убираем пробелы в начале и конце String cleaned = sentence.trim(); + // Проверяем минимальную длину NMEA сообщения + if (cleaned.length() < 6) { // Минимум: $GPGGA*XX + Log.w(TAG, "Слишком короткое NMEA сообщение: '" + cleaned + "'"); + return null; + } + + // Исправляем двойной $ ($$GNGGA -> $GNGGA) + if (cleaned.startsWith("$$")) { + cleaned = cleaned.substring(1); + Log.d(TAG, "Исправлен двойной $: " + cleaned); + } + + // Обрабатываем смешанные сообщения (например, VTG содержит GGA) + if (cleaned.contains("$G") && cleaned.indexOf("$G") > 0) { + // Находим первое полное NMEA сообщение + int firstDollar = cleaned.indexOf("$G"); + if (firstDollar > 0) { + String firstMessage = cleaned.substring(firstDollar); + int asteriskIndex = firstMessage.indexOf('*'); + if (asteriskIndex > 0) { + // Проверяем, что после * есть достаточно символов для контрольной суммы + if (asteriskIndex + 2 < firstMessage.length()) { + cleaned = firstMessage.substring(0, asteriskIndex + 3); + } else if (asteriskIndex + 1 < firstMessage.length()) { + cleaned = firstMessage.substring(0, asteriskIndex + 2); + } else { + cleaned = firstMessage.substring(0, asteriskIndex + 1); + } + Log.d(TAG, "Извлечено первое NMEA сообщение: " + cleaned); + } + } + } + // Убираем все символы после последнего * int asteriskIndex = cleaned.lastIndexOf('*'); if (asteriskIndex >= 0) { - cleaned = cleaned.substring(0, asteriskIndex + 3); // включаем * и 2 символа контрольной суммы + // Проверяем, что после * есть достаточно символов для контрольной суммы + if (asteriskIndex + 2 < cleaned.length()) { + cleaned = cleaned.substring(0, asteriskIndex + 3); // включаем * и 2 символа контрольной суммы + } else if (asteriskIndex + 1 < cleaned.length()) { + cleaned = cleaned.substring(0, asteriskIndex + 2); // включаем * и 1 символ контрольной суммы + } else { + cleaned = cleaned.substring(0, asteriskIndex + 1); // включаем только * + } } // Убираем все непечатаемые символы @@ -235,41 +293,47 @@ public class NMEAParser { * В гибридном режиме используем только курс и скорость */ private void parseRMC(String rmc) { -// Log.d(TAG, "Парсим RMC: " + rmc); -// Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern()); + Log.d(TAG, "Парсим RMC: " + rmc); + Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern()); Matcher matcher = RMC_PATTERN.matcher(rmc); if (matcher.matches()) { -// Log.d(TAG, "RMC совпадает с паттерном"); + Log.d(TAG, "RMC совпадает с паттерном"); - // Обрабатываем скорость - может быть пустым полем (теперь в группе 7) + // Проверяем статус валидности (группа 2) + String status = matcher.group(2); + boolean isValid = status != null && status.startsWith("A"); + Log.d(TAG, "RMC статус: " + status + " (валидный: " + isValid + ")"); + + // Обрабатываем скорость - может быть пустым полем (группа 7) double speed = 0.0; String speedStr = matcher.group(7); if (speedStr != null && !speedStr.trim().isEmpty()) { try { speed = Double.parseDouble(speedStr); } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0"); + Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0"); speed = 0.0; } } - // Обрабатываем курс - может быть пустым полем (теперь в группе 8) + // Обрабатываем курс - может быть пустым полем (группа 8) double course = 0.0; String courseStr = matcher.group(8); if (courseStr != null && !courseStr.trim().isEmpty()) { try { course = Double.parseDouble(courseStr); } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0"); + Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0"); course = 0.0; } } -// Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f", speed, course)); + Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f, valid=%s", speed, course, isValid)); // В гибридном режиме не обновляем координаты - if (!hybridMode) { + if (!hybridMode && isValid) { + Log.d(TAG, "Режим НЕ гибридный - обрабатываем координаты из RMC"); // Обрабатываем координаты - могут быть пустыми полями (группы 3,4,5,6) double latitude = 0.0; double longitude = 0.0; @@ -278,26 +342,43 @@ public class NMEAParser { String latDir = matcher.group(4); if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { latitude = parseCoordinate(latStr, latDir.equals("N")); + Log.d(TAG, "RMC широта: " + latStr + " " + latDir + " = " + latitude); } String lonStr = matcher.group(5); String lonDir = matcher.group(6); if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { longitude = parseCoordinate(lonStr, lonDir.equals("E")); + Log.d(TAG, "RMC долгота: " + lonStr + " " + lonDir + " = " + longitude); } + Log.d(TAG, "RMC устанавливаем координаты: lat=" + latitude + ", lon=" + longitude); ownVessel.setLatitude(latitude); ownVessel.setLongitude(longitude); + } else if (hybridMode) { + Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются"); + } else { + Log.d(TAG, "RMC данные невалидны (статус V) - координаты не обновляем"); } - ownVessel.setSpeed(speed); - ownVessel.setCourse(course); + // Обновляем скорость и курс только если данные валидны + if (isValid) { + ownVessel.setSpeed(speed); + ownVessel.setCourse(course); + } + + Log.d(TAG, "RMC обновлено судно: lat=" + ownVessel.getLatitude() + + ", lon=" + ownVessel.getLongitude() + + ", speed=" + speed + + ", course=" + course); if (listener != null) { listener.onVesselUpdated(ownVessel); } } else { -// Log.w(TAG, "RMC не совпадает с паттерном"); + Log.w(TAG, "RMC не совпадает с паттерном"); + Log.w(TAG, "Сообщение: '" + rmc + "'"); + Log.w(TAG, "Паттерн: " + RMC_PATTERN.pattern()); } } @@ -406,6 +487,8 @@ public class NMEAParser { systemType = "GLONASS"; } else if (gsv.startsWith("$GAGSV")) { systemType = "Galileo"; + } else if (gsv.startsWith("$GBGSV")) { + systemType = "BeiDou"; } else if (gsv.startsWith("$GNGSA")) { systemType = "GNSS"; } @@ -448,6 +531,10 @@ public class NMEAParser { case "Galileo": galileoSatellites = satellitesInView; break; + case "BeiDou": + // Пока не добавляем отдельный счетчик для BeiDou, считаем как GPS + gpsSatellites = Math.max(gpsSatellites, satellitesInView); + break; } // Обновляем общее количество спутников @@ -537,6 +624,45 @@ public class NMEAParser { } } + /** + * Парсит ZDA сообщение (Date and Time) + */ + private void parseZDA(String zda) { + Log.d(TAG, "Парсим ZDA: " + zda); + Matcher matcher = ZDA_PATTERN.matcher(zda); + if (matcher.matches()) { + try { + // Время (HHMMSS.SS) + String timeStr = matcher.group(1); + // День (DD) + int day = Integer.parseInt(matcher.group(2)); + // Месяц (MM) + int month = Integer.parseInt(matcher.group(3)); + // Год (YYYY) + int year = Integer.parseInt(matcher.group(4)); + // Часовой пояс (часы) + int timezoneHours = Integer.parseInt(matcher.group(5)); + // Часовой пояс (минуты) + int timezoneMinutes = Integer.parseInt(matcher.group(6)); + + Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d", + year, month, day, timeStr, timezoneHours, timezoneMinutes)); + + // Обновляем время последнего обновления + ownVessel.setLastUpdate(java.time.LocalDateTime.now()); + + if (listener != null) { + listener.onVesselUpdated(ownVessel); + } + + } catch (NumberFormatException e) { + Log.w(TAG, "Ошибка парсинга ZDA: " + e.getMessage()); + } + } else { + Log.w(TAG, "ZDA не совпадает с паттерном: " + zda); + } + } + /** * Парсит GSA сообщение (GPS DOP and Active Satellites) * КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников @@ -544,15 +670,18 @@ public class NMEAParser { private void parseGSA(String gsa) { Log.d(TAG, "Парсим GSA: " + gsa); Matcher matcher = GSA_PATTERN.matcher(gsa); + Matcher truncatedMatcher = GSA_TRUNCATED_PATTERN.matcher(gsa); + if (matcher.matches()) { -// Log.d(TAG, "GSA совпадает с паттерном"); + Log.d(TAG, "GSA совпадает с паттерном"); // Подсчитываем активные спутники (непустые поля) int activeSatellites = 0; - for (int i = 2; i <= 13; i++) { + for (int i = 3; i <= 14; i++) { // Группы 3-14 содержат ID спутников String satId = matcher.group(i); if (satId != null && !satId.trim().isEmpty() && !satId.equals("0")) { activeSatellites++; + Log.d(TAG, "Активный спутник: " + satId); } } @@ -561,35 +690,35 @@ public class NMEAParser { double hdop = 0.0; double vdop = 0.0; - String pdopStr = matcher.group(14); + String pdopStr = matcher.group(15); // PDOP в группе 15 if (pdopStr != null && !pdopStr.trim().isEmpty()) { try { pdop = Double.parseDouble(pdopStr); } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить PDOP: '" + pdopStr + "', используем 0.0"); + Log.w(TAG, "Не удалось распарсить PDOP: '" + pdopStr + "', используем 0.0"); } } - String hdopStr = matcher.group(15); + String hdopStr = matcher.group(16); // HDOP в группе 16 if (hdopStr != null && !hdopStr.trim().isEmpty()) { try { hdop = Double.parseDouble(hdopStr); } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить HDOP: '" + hdopStr + "', используем 0.0"); + Log.w(TAG, "Не удалось распарсить HDOP: '" + hdopStr + "', используем 0.0"); } } - String vdopStr = matcher.group(16); + String vdopStr = matcher.group(17); // VDOP в группе 17 if (vdopStr != null && !vdopStr.trim().isEmpty()) { try { vdop = Double.parseDouble(vdopStr); } catch (NumberFormatException e) { -// Log.w(TAG, "Не удалось распарсить VDOP: '" + vdopStr + "', используем 0.0"); + Log.w(TAG, "Не удалось распарсить VDOP: '" + vdopStr + "', используем 0.0"); } } -// Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f", -// activeSatellites, pdop, hdop, vdop)); + Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f", + activeSatellites, pdop, hdop, vdop)); // Обновляем информацию о спутниках ownVessel.setActiveSatellites(activeSatellites); @@ -604,13 +733,59 @@ public class NMEAParser { gpsLocationListener.setSatellitesInVessel(ownVessel); } + // Уведомляем слушателя о DOP + if (listener != null) { + listener.onDOPUpdated(pdop, hdop, vdop); + listener.onVesselUpdated(ownVessel); + } + } else if (truncatedMatcher.matches()) { + Log.d(TAG, "GSA совпадает с обрезанным паттерном"); + + // Обрабатываем обрезанное GSA сообщение + String pdopStr = truncatedMatcher.group(1); + String hdopStr = truncatedMatcher.group(2); + String vdopStr = truncatedMatcher.group(3); + + double pdop = 0.0; + double hdop = 0.0; + double vdop = 0.0; + + try { + if (pdopStr != null && !pdopStr.trim().isEmpty()) { + pdop = Double.parseDouble(pdopStr); + } + if (hdopStr != null && !hdopStr.trim().isEmpty()) { + hdop = Double.parseDouble(hdopStr); + } + if (vdopStr != null && !vdopStr.trim().isEmpty()) { + vdop = Double.parseDouble(vdopStr); + } + } catch (NumberFormatException e) { + Log.w(TAG, "Ошибка парсинга DOP в обрезанном GSA: " + e.getMessage()); + } + + Log.d(TAG, String.format("GSA (обрезанное): PDOP=%.2f, HDOP=%.2f, VDOP=%.2f", pdop, hdop, vdop)); + + // Обновляем DOP значения + ownVessel.setPdop(pdop); + ownVessel.setHdop(hdop); + ownVessel.setVdop(vdop); + + // Отправляем DOP значения в GPS Location Listener + if (gpsLocationListener != null) { + gpsLocationListener.setDOPValues(pdop, hdop, vdop); + } + // Уведомляем слушателя о DOP if (listener != null) { listener.onDOPUpdated(pdop, hdop, vdop); listener.onVesselUpdated(ownVessel); } } else { - Log.w(TAG, "GSA не совпадает с паттерном"); + Log.w(TAG, "GSA не совпадает ни с одним паттерном"); + Log.w(TAG, "Сообщение: '" + gsa + "'"); + Log.w(TAG, "Паттерн: " + GSA_PATTERN.pattern()); + Log.w(TAG, "Обрезанный паттерн: " + GSA_TRUNCATED_PATTERN.pattern()); } } diff --git a/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java b/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java new file mode 100644 index 0000000..27bb514 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/utils/SettingsManager.java @@ -0,0 +1,202 @@ +package com.grigowashere.aismap.utils; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +/** + * Менеджер настроек приложения + * Управляет сохранением и загрузкой настроек в SharedPreferences + */ +public class SettingsManager { + + private static final String TAG = "SettingsManager"; + private static final String PREFS_NAME = "AISMapSettings"; + + // Ключи для настроек + private static final String KEY_UDP_PORT = "udp_port"; + private static final String KEY_UDP_ENABLED = "udp_enabled"; + private static final String KEY_ANDROID_NMEA_ENABLED = "android_nmea_enabled"; + private static final String KEY_UDP_NMEA_ENABLED = "udp_nmea_enabled"; + private static final String KEY_DATA_MODE = "data_mode"; + + // Значения по умолчанию + private static final int DEFAULT_UDP_PORT = 10110; + private static final boolean DEFAULT_UDP_ENABLED = true; + private static final boolean DEFAULT_ANDROID_NMEA_ENABLED = true; + private static final boolean DEFAULT_UDP_NMEA_ENABLED = true; + private static final String DEFAULT_DATA_MODE = "hybrid"; + + // Режимы работы с данными + public static final String DATA_MODE_HYBRID = "hybrid"; + public static final String DATA_MODE_NMEA_ONLY = "nmea_only"; + public static final String DATA_MODE_ANDROID_ONLY = "android_only"; + + private Context context; + private SharedPreferences prefs; + + public SettingsManager(Context context) { + this.context = context; + this.prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + /** + * Получает UDP порт + */ + public int getUDPPort() { + return prefs.getInt(KEY_UDP_PORT, DEFAULT_UDP_PORT); + } + + /** + * Устанавливает UDP порт + */ + public void setUDPPort(int port) { + if (port < 1 || port > 65535) { + Log.w(TAG, "Некорректный порт: " + port + ", используем значение по умолчанию"); + port = DEFAULT_UDP_PORT; + } + prefs.edit().putInt(KEY_UDP_PORT, port).apply(); + Log.i(TAG, "UDP порт установлен: " + port); + } + + /** + * Проверяет, включен ли UDP слушатель + */ + public boolean isUDPEnabled() { + return prefs.getBoolean(KEY_UDP_ENABLED, DEFAULT_UDP_ENABLED); + } + + /** + * Включает/выключает UDP слушатель + */ + public void setUDPEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_UDP_ENABLED, enabled).apply(); + Log.i(TAG, "UDP слушатель: " + (enabled ? "включен" : "выключен")); + } + + /** + * Проверяет, включен ли Android NMEA + */ + public boolean isAndroidNMEAEnabled() { + return prefs.getBoolean(KEY_ANDROID_NMEA_ENABLED, DEFAULT_ANDROID_NMEA_ENABLED); + } + + /** + * Включает/выключает Android NMEA + */ + public void setAndroidNMEAEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_ANDROID_NMEA_ENABLED, enabled).apply(); + Log.i(TAG, "Android NMEA: " + (enabled ? "включен" : "выключен")); + } + + /** + * Проверяет, включен ли UDP NMEA + */ + public boolean isUDPNMEAEnabled() { + return prefs.getBoolean(KEY_UDP_NMEA_ENABLED, DEFAULT_UDP_NMEA_ENABLED); + } + + /** + * Включает/выключает UDP NMEA + */ + public void setUDPNMEAEnabled(boolean enabled) { + prefs.edit().putBoolean(KEY_UDP_NMEA_ENABLED, enabled).apply(); + Log.i(TAG, "UDP NMEA: " + (enabled ? "включен" : "выключен")); + } + + /** + * Получает режим работы с данными + */ + public String getDataMode() { + return prefs.getString(KEY_DATA_MODE, DEFAULT_DATA_MODE); + } + + /** + * Устанавливает режим работы с данными + */ + public void setDataMode(String mode) { + if (!isValidDataMode(mode)) { + Log.w(TAG, "Некорректный режим данных: " + mode + ", используем значение по умолчанию"); + mode = DEFAULT_DATA_MODE; + } + prefs.edit().putString(KEY_DATA_MODE, mode).apply(); + Log.i(TAG, "Режим данных установлен: " + mode); + } + + /** + * Проверяет корректность режима данных + */ + private boolean isValidDataMode(String mode) { + return DATA_MODE_HYBRID.equals(mode) || + DATA_MODE_NMEA_ONLY.equals(mode) || + DATA_MODE_ANDROID_ONLY.equals(mode); + } + + /** + * Проверяет, включен ли гибридный режим + */ + public boolean isHybridMode() { + return DATA_MODE_HYBRID.equals(getDataMode()); + } + + /** + * Проверяет, включен ли режим только NMEA + */ + public boolean isNMEAOnlyMode() { + return DATA_MODE_NMEA_ONLY.equals(getDataMode()); + } + + /** + * Проверяет, включен ли режим только Android GPS + */ + public boolean isAndroidOnlyMode() { + return DATA_MODE_ANDROID_ONLY.equals(getDataMode()); + } + + /** + * Сбрасывает все настройки к значениям по умолчанию + */ + public void resetToDefaults() { + prefs.edit() + .putInt(KEY_UDP_PORT, DEFAULT_UDP_PORT) + .putBoolean(KEY_UDP_ENABLED, DEFAULT_UDP_ENABLED) + .putBoolean(KEY_ANDROID_NMEA_ENABLED, DEFAULT_ANDROID_NMEA_ENABLED) + .putBoolean(KEY_UDP_NMEA_ENABLED, DEFAULT_UDP_NMEA_ENABLED) + .putString(KEY_DATA_MODE, DEFAULT_DATA_MODE) + .apply(); + Log.i(TAG, "Настройки сброшены к значениям по умолчанию"); + } + + /** + * Получает все настройки в виде строки для отладки + */ + public String getSettingsSummary() { + return String.format( + "UDP: порт=%d, включен=%s\n" + + "Android NMEA: %s\n" + + "UDP NMEA: %s\n" + + "Режим данных: %s", + getUDPPort(), + isUDPEnabled() ? "да" : "нет", + isAndroidNMEAEnabled() ? "включен" : "выключен", + isUDPNMEAEnabled() ? "включен" : "выключен", + getDataMode() + ); + } + + /** + * Проверяет, нужно ли перезапустить UDP слушатель + */ + public boolean shouldRestartUDP(int currentPort, boolean currentEnabled) { + return getUDPPort() != currentPort || isUDPEnabled() != currentEnabled; + } + + /** + * Проверяет, нужно ли перезапустить NMEA парсер + */ + public boolean shouldRestartNMEA(boolean currentAndroidNMEA, boolean currentUDPNMEA, String currentDataMode) { + return isAndroidNMEAEnabled() != currentAndroidNMEA || + isUDPNMEAEnabled() != currentUDPNMEA || + !getDataMode().equals(currentDataMode); + } +} diff --git a/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java b/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java index 52e3835..1ade483 100644 --- a/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java +++ b/app/src/main/java/com/grigowashere/aismap/view/BaseDockWidget.java @@ -53,6 +53,12 @@ public abstract class BaseDockWidget extends FrameLayout { } protected OnDockResizeListener dockResizeListener; + // Интерфейс для уведомления об изменении состояния docked + public interface OnDockStateChangeListener { + void onDockStateChanged(boolean isDocked, boolean isTop); + } + protected OnDockStateChangeListener dockStateChangeListener; + public BaseDockWidget(Context context) { super(context); init(); @@ -246,12 +252,23 @@ public abstract class BaseDockWidget extends FrameLayout { dockHeightPx = newHeight; setLayoutParams(lp); - // Если закреплен снизу, нужно также изменить позицию Y - if (!dockTop) { + // Корректируем позицию Y в зависимости от позиции закрепления + if (dockTop) { + // Если закреплен сверху, позиция Y всегда должна быть 0 + setY(0); + } else { + // Если закреплен снизу, позиция Y должна быть (parentHeight - newHeight) float newY = ((ViewGroup) getParent()).getHeight() - newHeight; setY(newY); } + // Перепозиционируем все docked виджеты после изменения размера + ViewGroup parent = (ViewGroup) getParent(); + if (parent != null) { + repositionAllDockedWidgets(parent); + } + + // Уведомляем об изменении размера if (dockResizeListener != null) { dockResizeListener.onDockResize(newHeight); } @@ -362,9 +379,13 @@ public abstract class BaseDockWidget extends FrameLayout { float endX = targetX; float endY = targetY; - // Если доким в нижнюю часть, корректируем позицию Y - if (docked && !top) { - endY = parentHeight - dockHeight; + // Если доким, вычисляем правильную позицию с учетом других docked виджетов + if (docked) { + endX = 0; + endY = calculateDockPosition(top); + } else { + // При переходе в movable режим сбрасываем размер до дефолтного + dockHeightPx = 0; } // Сохраняем финальные значения для использования в lambda и inner class @@ -414,6 +435,11 @@ public abstract class BaseDockWidget extends FrameLayout { morphAnimator.start(); this.isDocked = docked; isMorphing = true; + + // Уведомляем об изменении состояния docked + if (dockStateChangeListener != null) { + dockStateChangeListener.onDockStateChanged(docked, top); + } } public boolean isDocked() { @@ -432,10 +458,101 @@ public abstract class BaseDockWidget extends FrameLayout { this.dockResizeListener = listener; } + public void setOnDockStateChangeListener(OnDockStateChangeListener listener) { + this.dockStateChangeListener = listener; + } + protected float dp(float dp) { return dp * getResources().getDisplayMetrics().density; } + /** + * Вычисляет правильную позицию для докинга с учетом других docked виджетов + */ + protected float calculateDockPosition(boolean dockTop) { + ViewGroup parent = (ViewGroup) getParent(); + if (parent == null) return 0; + + int dockHeight = (int) dp(DEFAULT_DOCK_HEIGHT_DP); + float y = 0; + + if (dockTop) { + // Доким сверху - начинаем с позиции 0 + y = 0; + + // Проверяем другие виджеты сверху + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + if (child != this && child instanceof BaseDockWidget) { + BaseDockWidget otherWidget = (BaseDockWidget) child; + if (otherWidget.isDocked() && otherWidget.isDockTop()) { + // Если другой виджет уже docked сверху, ставим наш под ним + y = otherWidget.getY() + otherWidget.getHeight(); + break; + } + } + } + } else { + // Доким снизу - начинаем с нижней позиции + y = parent.getHeight() - dockHeight; + + // Проверяем другие виджеты снизу + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + if (child != this && child instanceof BaseDockWidget) { + BaseDockWidget otherWidget = (BaseDockWidget) child; + if (otherWidget.isDocked() && !otherWidget.isDockTop()) { + // Если другой виджет уже docked снизу, ставим наш над ним + y = otherWidget.getY() - dockHeight; + break; + } + } + } + } + + return y; + } + + /** + * Перепозиционирует все docked виджеты, чтобы они прижались к краям + */ + public static void repositionAllDockedWidgets(ViewGroup parent) { + if (parent == null) return; + + // Собираем все docked виджеты сверху + java.util.List topWidgets = new java.util.ArrayList<>(); + java.util.List bottomWidgets = new java.util.ArrayList<>(); + + for (int i = 0; i < parent.getChildCount(); i++) { + View child = parent.getChildAt(i); + if (child instanceof BaseDockWidget) { + BaseDockWidget widget = (BaseDockWidget) child; + if (widget.isDocked()) { + if (widget.isDockTop()) { + topWidgets.add(widget); + } else { + bottomWidgets.add(widget); + } + } + } + } + + // Перепозиционируем виджеты сверху + float currentY = 0; + for (BaseDockWidget widget : topWidgets) { + widget.setY(currentY); + currentY += widget.getHeight(); + } + + // Перепозиционируем виджеты снизу + currentY = parent.getHeight(); + for (int i = bottomWidgets.size() - 1; i >= 0; i--) { + BaseDockWidget widget = bottomWidgets.get(i); + currentY -= widget.getHeight(); + widget.setY(currentY); + } + } + // Абстрактные методы для переопределения в наследниках protected abstract void onDrawDock(Canvas canvas); protected abstract void onDrawCircle(Canvas canvas); 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 b7855ad..832261e 100644 --- a/app/src/main/java/com/grigowashere/aismap/view/CompassView.java +++ b/app/src/main/java/com/grigowashere/aismap/view/CompassView.java @@ -80,7 +80,7 @@ public class CompassView extends BaseDockWidget { // Прямая шкала (dock-режим) @Override protected void onDrawDock(Canvas canvas) { - Log.d(TAG, "onDrawDock called, width=" + getWidth() + ", height=" + getHeight()); + // Log.d(TAG, "onDrawDock called, width=" + getWidth() + ", height=" + getHeight()); float w = getWidth(); float h = getHeight(); @@ -192,13 +192,13 @@ public class CompassView extends BaseDockWidget { // Круглый компас (draggable-режим) @Override protected void onDrawCircle(Canvas canvas) { - Log.d(TAG, "onDrawCircle called, width=" + getWidth() + ", height=" + getHeight()); + //Log.d(TAG, "onDrawCircle called, width=" + getWidth() + ", height=" + getHeight()); float w = getWidth(); float h = getHeight(); if (w <= 0 || h <= 0) { - Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h); + // Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h); return; } diff --git a/app/src/main/java/com/grigowashere/aismap/view/CoordinatesDockWidget.java b/app/src/main/java/com/grigowashere/aismap/view/CoordinatesDockWidget.java new file mode 100644 index 0000000..fa0a185 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/view/CoordinatesDockWidget.java @@ -0,0 +1,275 @@ +package com.grigowashere.aismap.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.Log; +import com.grigowashere.aismap.models.Vessel; + +public class CoordinatesDockWidget extends BaseDockWidget { + private static final String TAG = "CoordinatesDockWidget"; + + // Цвета + private static final int BACKGROUND_COLOR = 0xE6000000; // Полупрозрачный черный + private static final int TEXT_COLOR = 0xFFFFFFFF; // Белый + private static final int ACCENT_COLOR = 0xFF4CAF50; // Зеленый + private static final int WARNING_COLOR = 0xFFFF9800; // Оранжевый + private static final int ERROR_COLOR = 0xFFF44336; // Красный + + // Кисти + private Paint backgroundPaint; + private Paint textPaint; + private Paint accentPaint; + private Paint warningPaint; + private Paint errorPaint; + + // Данные для отображения + private Vessel vessel; + private String coordinatesText = "Координаты: --"; + private String sogText = "SOG: --"; + private String cogText = "COG: --"; + + public CoordinatesDockWidget(Context context) { + super(context); + init(); + } + + public CoordinatesDockWidget(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + private void init() { + // Инициализируем кисти + backgroundPaint = new Paint(); + backgroundPaint.setColor(BACKGROUND_COLOR); + backgroundPaint.setStyle(Paint.Style.FILL); + backgroundPaint.setAntiAlias(true); + + textPaint = new Paint(); + textPaint.setColor(TEXT_COLOR); + textPaint.setTextSize(dp(14)); + textPaint.setTypeface(Typeface.DEFAULT_BOLD); + textPaint.setAntiAlias(true); + + accentPaint = new Paint(); + accentPaint.setColor(ACCENT_COLOR); + accentPaint.setTextSize(dp(14)); + accentPaint.setTypeface(Typeface.DEFAULT_BOLD); + accentPaint.setAntiAlias(true); + + warningPaint = new Paint(); + warningPaint.setColor(WARNING_COLOR); + warningPaint.setTextSize(dp(14)); + warningPaint.setTypeface(Typeface.DEFAULT_BOLD); + warningPaint.setAntiAlias(true); + + errorPaint = new Paint(); + errorPaint.setColor(ERROR_COLOR); + errorPaint.setTextSize(dp(14)); + errorPaint.setTypeface(Typeface.DEFAULT_BOLD); + errorPaint.setAntiAlias(true); + + // Устанавливаем фон для видимости (как в CompassView) + setBackgroundColor(android.graphics.Color.argb(200, 0, 0, 0)); + } + + /** + * Обновляет данные судна + */ + public void updateVessel(Vessel vessel) { + Log.d(TAG, "updateVessel called with vessel: " + (vessel != null ? "not null" : "null")); + if (vessel != null) { + Log.d(TAG, "Vessel data: lat=" + vessel.getLatitude() + + ", lon=" + vessel.getLongitude() + + ", speed=" + vessel.getSpeed() + + ", course=" + vessel.getCourse()); + } + this.vessel = vessel; + updateDisplayText(); + invalidate(); + } + + /** + * Обновляет текст для отображения + */ + private void updateDisplayText() { + if (vessel == null) { + coordinatesText = "Координаты: --"; + sogText = "SOG: --"; + cogText = "COG: --"; + return; + } + + // Координаты + if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { + coordinatesText = String.format("📍 %.6f, %.6f", + vessel.getLatitude(), vessel.getLongitude()); + } else { + coordinatesText = "📍 Координаты: --"; + } + + // SOG (Speed Over Ground) + if (vessel.getSpeed() > 0) { + sogText = String.format("⚡ SOG: %.1f уз", vessel.getSpeed()); + } else { + sogText = "⚡ SOG: --"; + } + + // COG (Course Over Ground) + if (vessel.getCourse() > 0) { + cogText = String.format("🧭 COG: %.1f°", vessel.getCourse()); + } else { + cogText = "🧭 COG: --"; + } + } + + @Override + protected void onDrawDock(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + + Log.d(TAG, "onDrawDock called, width=" + width + ", height=" + height); + + if (width <= 0 || height <= 0) { + Log.w(TAG, "Invalid dimensions: width=" + width + ", height=" + height); + return; + } + + // Рисуем фон + canvas.drawRect(0, 0, width, height, backgroundPaint); + + // Вычисляем позиции для текста + float textSize = dp(14); + float lineHeight = textSize * 1.2f; + float startY = (height - (lineHeight * 3)) / 2 + textSize; + + // Определяем цвета в зависимости от качества данных + Paint coordinatesPaint = getCoordinatesPaint(); + Paint sogPaint = getSOGPaint(); + Paint cogPaint = getCOGPaint(); + + // Рисуем тестовый заголовок для проверки видимости + Paint testPaint = new Paint(); + testPaint.setColor(android.graphics.Color.WHITE); + testPaint.setTextSize(dp(16)); + testPaint.setTypeface(android.graphics.Typeface.DEFAULT_BOLD); + testPaint.setAntiAlias(true); + canvas.drawText("КООРДИНАТЫ", dp(16), dp(20), testPaint); + + // Рисуем текст + canvas.drawText(coordinatesText, dp(16), startY, coordinatesPaint); + canvas.drawText(sogText, dp(16), startY + lineHeight, sogPaint); + canvas.drawText(cogText, dp(16), startY + lineHeight * 2, cogPaint); + + // Рисуем разделительную линию сверху, если закреплен снизу + if (!isDockTop()) { + Paint linePaint = new Paint(); + linePaint.setColor(ACCENT_COLOR); + linePaint.setStrokeWidth(dp(2)); + canvas.drawLine(0, 0, width, 0, linePaint); + } + } + + @Override + protected void onDrawCircle(Canvas canvas) { + int width = getWidth(); + int height = getHeight(); + int centerX = width / 2; + int centerY = height / 2; + int radius = Math.min(width, height) / 2 - (int)dp(8); + + // Рисуем фон + canvas.drawCircle(centerX, centerY, radius, backgroundPaint); + + // Рисуем рамку + Paint borderPaint = new Paint(); + borderPaint.setColor(ACCENT_COLOR); + borderPaint.setStyle(Paint.Style.STROKE); + borderPaint.setStrokeWidth(dp(3)); + borderPaint.setAntiAlias(true); + canvas.drawCircle(centerX, centerY, radius, borderPaint); + + // Вычисляем позиции для текста в круге + float textSize = dp(12); + float lineHeight = textSize * 1.3f; + float startY = centerY - lineHeight; + + // Определяем цвета + Paint coordinatesPaint = getCoordinatesPaint(); + Paint sogPaint = getSOGPaint(); + Paint cogPaint = getCOGPaint(); + + // Центрируем текст + Rect textBounds = new Rect(); + + // Координаты + coordinatesPaint.getTextBounds(coordinatesText, 0, coordinatesText.length(), textBounds); + canvas.drawText(coordinatesText, centerX - textBounds.width() / 2f, startY, coordinatesPaint); + + // SOG + sogPaint.getTextBounds(sogText, 0, sogText.length(), textBounds); + canvas.drawText(sogText, centerX - textBounds.width() / 2f, startY + lineHeight, sogPaint); + + // COG + cogPaint.getTextBounds(cogText, 0, cogText.length(), textBounds); + canvas.drawText(cogText, centerX - textBounds.width() / 2f, startY + lineHeight * 2, cogPaint); + } + + /** + * Определяет цвет для отображения координат + */ + private Paint getCoordinatesPaint() { + if (vessel == null || vessel.getLatitude() == 0 || vessel.getLongitude() == 0) { + return errorPaint; + } + + // Проверяем точность GPS + if (vessel.getAccuracy() > 0) { + if (vessel.getAccuracy() <= 5) { + return accentPaint; // Высокая точность - зеленый + } else if (vessel.getAccuracy() <= 20) { + return warningPaint; // Средняя точность - оранжевый + } else { + return errorPaint; // Низкая точность - красный + } + } + + return textPaint; // По умолчанию - белый + } + + /** + * Определяет цвет для отображения SOG + */ + private Paint getSOGPaint() { + if (vessel == null || vessel.getSpeed() <= 0) { + return errorPaint; + } + + return accentPaint; // Если есть данные о скорости - зеленый + } + + /** + * Определяет цвет для отображения COG + */ + private Paint getCOGPaint() { + if (vessel == null || vessel.getCourse() <= 0) { + return errorPaint; + } + + return accentPaint; // Если есть данные о курсе - зеленый + } + + /** + * Получает высоту виджета в dock-режиме + */ + public int getDockHeight() { + if (isDocked()) { + return getHeight(); + } + return (int) dp(DEFAULT_DOCK_HEIGHT_DP); + } +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2f0bfcf..a325d7d 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -12,13 +12,15 @@ android:layout_height="match_parent" /> + @@ -29,16 +31,28 @@ android:layout_height="wrap_content" android:text="Центр на судне" android:textSize="12sp" - android:minWidth="100dp" /> + android:minWidth="120dp" + android:background="@android:color/white" + android:layout_marginBottom="8dp" />