generated from Grigo/AndroidTemplate
Created ship vectors (not added yet)
Created menu Created udp support Created DockWidgets for compass and SOG/COG
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.grigowashere.aismap;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -9,8 +10,10 @@ import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
@@ -24,6 +27,9 @@ import com.grigowashere.aismap.models.Vessel;
|
||||
import com.grigowashere.aismap.models.AISVessel;
|
||||
import com.grigowashere.aismap.sensors.CompassSensor;
|
||||
import com.grigowashere.aismap.view.CompassView;
|
||||
import com.grigowashere.aismap.view.CoordinatesDockWidget;
|
||||
import com.grigowashere.aismap.view.BaseDockWidget;
|
||||
import com.grigowashere.aismap.utils.SettingsManager;
|
||||
import com.yandex.mapkit.mapview.MapView;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
@@ -32,6 +38,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
private static final int PERMISSION_REQUEST_CODE = 1001;
|
||||
private static final int SETTINGS_REQUEST_CODE = 1002;
|
||||
|
||||
// Статическая переменная для отслеживания инициализации Яндекс.Карт
|
||||
private static boolean isYandexMapsInitialized = false;
|
||||
@@ -40,11 +47,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
private MapController mapController;
|
||||
private MapInterface mapInterface;
|
||||
private MapView mapView;
|
||||
private SettingsManager settingsManager;
|
||||
|
||||
private Button btnCenterOnVessel;
|
||||
private Button btnTestCompass;
|
||||
private Button btnMapOrientation;
|
||||
private Button btnSettings;
|
||||
private LinearLayout controlPanel;
|
||||
private CompassView compassView;
|
||||
private CompassSensor compassSensor;
|
||||
private CoordinatesDockWidget coordinatesWidget;
|
||||
|
||||
// BottomSheet для отображения информации о нашем судне
|
||||
private BottomSheetDialog ownVesselBottomSheet;
|
||||
@@ -79,8 +90,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void initializeViews() {
|
||||
mapView = findViewById(R.id.map_view);
|
||||
btnCenterOnVessel = findViewById(R.id.btn_center_vessel);
|
||||
btnTestCompass = findViewById(R.id.btn_test_compass);
|
||||
btnMapOrientation = findViewById(R.id.btn_map_orientation);
|
||||
btnSettings = findViewById(R.id.btn_settings);
|
||||
controlPanel = findViewById(R.id.control_panel);
|
||||
compassView = findViewById(R.id.compass_view);
|
||||
coordinatesWidget = findViewById(R.id.coordinates_widget);
|
||||
|
||||
// Инициализируем магнитный компас
|
||||
compassSensor = new CompassSensor(this);
|
||||
@@ -88,11 +102,13 @@ public class MainActivity extends AppCompatActivity {
|
||||
initializeBottomSheet();
|
||||
setupButtonListeners();
|
||||
setupCompass();
|
||||
setupCoordinatesWidget();
|
||||
}
|
||||
|
||||
private void setupButtonListeners() {
|
||||
btnCenterOnVessel.setOnClickListener(v -> 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<AISVessel> 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<AISVessel> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 : "не установлен"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<BaseDockWidget> topWidgets = new java.util.ArrayList<>();
|
||||
java.util.List<BaseDockWidget> 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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user