Created ship vectors (not added yet)

Created menu
Created udp support
Created DockWidgets for compass and SOG/COG
This commit is contained in:
2025-09-03 15:40:02 +03:00
parent 2734560160
commit 25b1dabf73
70 changed files with 3145 additions and 293 deletions
+6
View File
@@ -43,6 +43,12 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".SettingsActivity"
android:exported="false"
android:configChanges="orientation|screenSize|keyboardHidden"
android:theme="@style/Theme.AISMap" />
<!-- Мета-данные для Яндекс.Карт --> <!-- Мета-данные для Яндекс.Карт -->
<meta-data <meta-data
android:name="com.yandex.mapkit.ApiKey" android:name="com.yandex.mapkit.ApiKey"
@@ -1,6 +1,7 @@
package com.grigowashere.aismap; package com.grigowashere.aismap;
import android.Manifest; import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@@ -9,8 +10,10 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import android.view.ViewGroup;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat; 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.models.AISVessel;
import com.grigowashere.aismap.sensors.CompassSensor; import com.grigowashere.aismap.sensors.CompassSensor;
import com.grigowashere.aismap.view.CompassView; 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 com.yandex.mapkit.mapview.MapView;
import java.util.List; import java.util.List;
import java.util.ArrayList; import java.util.ArrayList;
@@ -32,6 +38,7 @@ public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity"; private static final String TAG = "MainActivity";
private static final int PERMISSION_REQUEST_CODE = 1001; private static final int PERMISSION_REQUEST_CODE = 1001;
private static final int SETTINGS_REQUEST_CODE = 1002;
// Статическая переменная для отслеживания инициализации Яндекс.Карт // Статическая переменная для отслеживания инициализации Яндекс.Карт
private static boolean isYandexMapsInitialized = false; private static boolean isYandexMapsInitialized = false;
@@ -40,11 +47,15 @@ public class MainActivity extends AppCompatActivity {
private MapController mapController; private MapController mapController;
private MapInterface mapInterface; private MapInterface mapInterface;
private MapView mapView; private MapView mapView;
private SettingsManager settingsManager;
private Button btnCenterOnVessel; private Button btnCenterOnVessel;
private Button btnTestCompass; private Button btnMapOrientation;
private Button btnSettings;
private LinearLayout controlPanel;
private CompassView compassView; private CompassView compassView;
private CompassSensor compassSensor; private CompassSensor compassSensor;
private CoordinatesDockWidget coordinatesWidget;
// BottomSheet для отображения информации о нашем судне // BottomSheet для отображения информации о нашем судне
private BottomSheetDialog ownVesselBottomSheet; private BottomSheetDialog ownVesselBottomSheet;
@@ -79,8 +90,11 @@ public class MainActivity extends AppCompatActivity {
private void initializeViews() { private void initializeViews() {
mapView = findViewById(R.id.map_view); mapView = findViewById(R.id.map_view);
btnCenterOnVessel = findViewById(R.id.btn_center_vessel); 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); compassView = findViewById(R.id.compass_view);
coordinatesWidget = findViewById(R.id.coordinates_widget);
// Инициализируем магнитный компас // Инициализируем магнитный компас
compassSensor = new CompassSensor(this); compassSensor = new CompassSensor(this);
@@ -88,11 +102,13 @@ public class MainActivity extends AppCompatActivity {
initializeBottomSheet(); initializeBottomSheet();
setupButtonListeners(); setupButtonListeners();
setupCompass(); setupCompass();
setupCoordinatesWidget();
} }
private void setupButtonListeners() { private void setupButtonListeners() {
btnCenterOnVessel.setOnClickListener(v -> centerOnVessel()); 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); // Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info);
@@ -114,6 +130,18 @@ public class MainActivity extends AppCompatActivity {
// Настраиваем слушатель изменения размера док-виджета // Настраиваем слушатель изменения размера док-виджета
compassView.setOnDockResizeListener(newHeight -> { compassView.setOnDockResizeListener(newHeight -> {
Log.d(TAG, "Compass dock height changed to: " + 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 //smt changed
// Настраиваем магнитный компас // Настраиваем магнитный компас
@@ -144,6 +172,48 @@ public class MainActivity extends AppCompatActivity {
// Принудительная отрисовка // Принудительная отрисовка
compassView.invalidate(); 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) { private void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels) {
@@ -248,6 +318,9 @@ public class MainActivity extends AppCompatActivity {
} }
private void initializeControllers() { private void initializeControllers() {
// Инициализация менеджера настроек
settingsManager = new SettingsManager(this);
// Инициализация главного контроллера // Инициализация главного контроллера
appController = new AppController(this); appController = new AppController(this);
@@ -292,10 +365,8 @@ public class MainActivity extends AppCompatActivity {
} }
private void startControllers() { private void startControllers() {
// Включаем GPS и UDP по умолчанию // Загружаем настройки и применяем их
appController.setGPSLocationEnabled(true); applySettings();
appController.setAndroidNMEAEnabled(true);
appController.setUDPEnabled(true);
// Запускаем все слушатели // Запускаем все слушатели
appController.startAllListeners(); appController.startAllListeners();
@@ -331,6 +402,11 @@ public class MainActivity extends AppCompatActivity {
// tvStatus.setText("Статус: GPS активен, данные получены"); // tvStatus.setText("Статус: GPS активен, данные получены");
// } // }
// Обновляем виджет координат
if (coordinatesWidget != null) {
coordinatesWidget.updateVessel(vessel);
}
// Обновляем BottomSheet, если он открыт // Обновляем BottomSheet, если он открыт
if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) {
updateBottomSheetUI(); updateBottomSheetUI();
@@ -387,43 +463,94 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "Карта центрирована на судне", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Карта центрирована на судне", Toast.LENGTH_SHORT).show();
} }
private void testCompass() { private void toggleMapOrientation() {
if (compassView != null) { // TODO: Реализовать переключение ориентации карты
// Создаем тестовые AIS суда // Состояния: север, курс, компас
List<AISVessel> testVessels = new ArrayList<>(); Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show();
}
AISVessel testVessel1 = new AISVessel("123456789");
testVessel1.setLatitude(59.9343); private void showSettings() {
testVessel1.setLongitude(30.3351); Intent intent = new Intent(this, SettingsActivity.class);
testVessel1.setCourse(45); startActivityForResult(intent, SETTINGS_REQUEST_CODE);
testVessel1.setSpeed(10); }
testVessel1.setNavigationalStatus("under way using engine");
testVessels.add(testVessel1); /**
* Обновляет позицию панели управления в зависимости от состояния docked виджетов
AISVessel testVessel2 = new AISVessel("987654321"); */
testVessel2.setLatitude(59.9343); private void updateControlPanelPosition() {
testVessel2.setLongitude(30.3351); if (controlPanel != null) {
testVessel2.setCourse(180); runOnUiThread(() -> {
testVessel2.setSpeed(5); // Получаем текущие параметры layout
testVessel2.setNavigationalStatus("at anchor"); android.widget.RelativeLayout.LayoutParams params =
testVessels.add(testVessel2); (android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams();
compassView.updateNearbyVessels(testVessels); int topMargin = dpToPx(16); // По умолчанию отступ сверху
int bottomMargin = dpToPx(16); // По умолчанию отступ снизу
// Проверяем доступность магнитного компаса
if (compassSensor.isAvailable()) { // Вычисляем общую высоту всех docked виджетов сверху
Toast.makeText(this, "Магнитный компас доступен и работает", Toast.LENGTH_SHORT).show(); int totalTopHeight = 0;
} else { if (compassView != null && compassView.isDocked() && compassView.isDockTop()) {
Toast.makeText(this, "Магнитный компас недоступен", Toast.LENGTH_SHORT).show(); 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() { private void clearAIS() {
appController.clearAISVessels(); appController.clearAISVessels();
Toast.makeText(this, "AIS суда очищены", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "AIS суда очищены", Toast.LENGTH_SHORT).show();
} }
/**
* Конвертирует dp в px
*/
private int dpToPx(int dp) {
return (int) (dp * getResources().getDisplayMetrics().density);
}
@Override @Override
protected void onStart() { protected void onStart() {
super.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 @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 isUDPEnabled;
private boolean isAndroidNMEAEnabled; private boolean isAndroidNMEAEnabled;
private boolean isUDPNMEAEnabled;
private boolean isGPSLocationEnabled; private boolean isGPSLocationEnabled;
private int udpPort;
private String dataMode;
// Callback для обновления UI // Callback для обновления UI
private UIUpdateCallback uiUpdateCallback; private UIUpdateCallback uiUpdateCallback;
@@ -82,7 +85,8 @@ public class AppController implements
nmeaParser.setHybridMode(true); nmeaParser.setHybridMode(true);
// Инициализация UDP слушателя (порт 10110 - стандартный для AIS) // Инициализация UDP слушателя (порт 10110 - стандартный для AIS)
udpListener = new UDPListener(10110); udpPort = 10110;
udpListener = new UDPListener(udpPort);
udpListener.setCallback(this); udpListener.setCallback(this);
// Инициализация Android NMEA слушателя (для курса, скорости, DOP) // Инициализация 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 * Отправляет данные по UDP
@@ -328,7 +308,20 @@ public class AppController implements
@Override @Override
public void onVesselUpdated(Vessel vessel) { 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) { if (vessel.getCourse() > 0) {
ownVessel.setCourse(vessel.getCourse()); ownVessel.setCourse(vessel.getCourse());
updateCompass(); // Обновляем компас при изменении курса updateCompass(); // Обновляем компас при изменении курса
@@ -347,6 +340,20 @@ public class AppController implements
", speed=" + vessel.getSpeed() + ", speed=" + vessel.getSpeed() +
", satellites=" + vessel.getSatellites()); ", 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 // Обновляем UI
if (uiUpdateCallback != null) { if (uiUpdateCallback != null) {
uiUpdateCallback.onVesselPositionUpdated(ownVessel); uiUpdateCallback.onVesselPositionUpdated(ownVessel);
@@ -592,4 +599,111 @@ public class AppController implements
executor.shutdown(); 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( 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( private static final Pattern VTG_PATTERN = Pattern.compile(
@@ -44,7 +44,17 @@ public class NMEAParser {
// Паттерн для GSA сообщения (DOP и активные спутники) // Паттерн для GSA сообщения (DOP и активные спутники)
private static final Pattern GSA_PATTERN = Pattern.compile( 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( private static final Pattern AIS_PATTERN = Pattern.compile(
@@ -97,7 +107,9 @@ public class NMEAParser {
*/ */
public void setHybridMode(boolean enabled) { public void setHybridMode(boolean enabled) {
this.hybridMode = 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); String cleanedSentence = cleanNMEASentence(nmeaSentence);
if (cleanedSentence == null) {
Log.w(TAG, "NMEA сообщение не удалось очистить или слишком короткое: " + nmeaSentence);
return;
}
Log.d(TAG, "Парсим NMEA: " + cleanedSentence); Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
try { try {
@@ -121,12 +137,14 @@ public class NMEAParser {
parseVTG(cleanedSentence); parseVTG(cleanedSentence);
} else if (cleanedSentence.startsWith("$GPGLL") || cleanedSentence.startsWith("$GNGLL")) { } else if (cleanedSentence.startsWith("$GPGLL") || cleanedSentence.startsWith("$GNGLL")) {
parseGLL(cleanedSentence); 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); parseGSV(cleanedSentence);
} else if (cleanedSentence.startsWith("$GNGNS")) { } else if (cleanedSentence.startsWith("$GNGNS")) {
parseGNS(cleanedSentence); parseGNS(cleanedSentence);
} else if (cleanedSentence.startsWith("$GPGSA") || cleanedSentence.startsWith("$GNGSA")) { } else if (cleanedSentence.startsWith("$GPGSA") || cleanedSentence.startsWith("$GNGSA")) {
parseGSA(cleanedSentence); parseGSA(cleanedSentence);
} else if (cleanedSentence.startsWith("$GPZDA") || cleanedSentence.startsWith("$GNZDA")) {
parseZDA(cleanedSentence);
} else if (cleanedSentence.startsWith("!AIVDM")) { } else if (cleanedSentence.startsWith("!AIVDM")) {
parseAIS(cleanedSentence); parseAIS(cleanedSentence);
} else { } else {
@@ -144,17 +162,57 @@ public class NMEAParser {
* Очищает NMEA сообщение от лишних символов * Очищает NMEA сообщение от лишних символов
*/ */
private String cleanNMEASentence(String sentence) { private String cleanNMEASentence(String sentence) {
if (sentence == null) { if (sentence == null || sentence.trim().isEmpty()) {
return null; return null;
} }
// Убираем пробелы в начале и конце // Убираем пробелы в начале и конце
String cleaned = sentence.trim(); 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('*'); int asteriskIndex = cleaned.lastIndexOf('*');
if (asteriskIndex >= 0) { 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) { private void parseRMC(String rmc) {
// Log.d(TAG, "Парсим RMC: " + rmc); Log.d(TAG, "Парсим RMC: " + rmc);
// Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern()); Log.d(TAG, "Применяем паттерн RMC: " + RMC_PATTERN.pattern());
Matcher matcher = RMC_PATTERN.matcher(rmc); Matcher matcher = RMC_PATTERN.matcher(rmc);
if (matcher.matches()) { 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; double speed = 0.0;
String speedStr = matcher.group(7); String speedStr = matcher.group(7);
if (speedStr != null && !speedStr.trim().isEmpty()) { if (speedStr != null && !speedStr.trim().isEmpty()) {
try { try {
speed = Double.parseDouble(speedStr); speed = Double.parseDouble(speedStr);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0"); Log.w(TAG, "Не удалось распарсить скорость RMC: '" + speedStr + "', используем 0.0");
speed = 0.0; speed = 0.0;
} }
} }
// Обрабатываем курс - может быть пустым полем (теперь в группе 8) // Обрабатываем курс - может быть пустым полем (группа 8)
double course = 0.0; double course = 0.0;
String courseStr = matcher.group(8); String courseStr = matcher.group(8);
if (courseStr != null && !courseStr.trim().isEmpty()) { if (courseStr != null && !courseStr.trim().isEmpty()) {
try { try {
course = Double.parseDouble(courseStr); course = Double.parseDouble(courseStr);
} catch (NumberFormatException e) { } catch (NumberFormatException e) {
// Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0"); Log.w(TAG, "Не удалось распарсить курс: '" + courseStr + "', используем 0.0");
course = 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) // Обрабатываем координаты - могут быть пустыми полями (группы 3,4,5,6)
double latitude = 0.0; double latitude = 0.0;
double longitude = 0.0; double longitude = 0.0;
@@ -278,26 +342,43 @@ public class NMEAParser {
String latDir = matcher.group(4); String latDir = matcher.group(4);
if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) { if (latStr != null && !latStr.trim().isEmpty() && latDir != null && !latDir.trim().isEmpty()) {
latitude = parseCoordinate(latStr, latDir.equals("N")); latitude = parseCoordinate(latStr, latDir.equals("N"));
Log.d(TAG, "RMC широта: " + latStr + " " + latDir + " = " + latitude);
} }
String lonStr = matcher.group(5); String lonStr = matcher.group(5);
String lonDir = matcher.group(6); String lonDir = matcher.group(6);
if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) { if (lonStr != null && !lonStr.trim().isEmpty() && lonDir != null && !lonDir.trim().isEmpty()) {
longitude = parseCoordinate(lonStr, lonDir.equals("E")); 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.setLatitude(latitude);
ownVessel.setLongitude(longitude); 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) { if (listener != null) {
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} else { } 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"; systemType = "GLONASS";
} else if (gsv.startsWith("$GAGSV")) { } else if (gsv.startsWith("$GAGSV")) {
systemType = "Galileo"; systemType = "Galileo";
} else if (gsv.startsWith("$GBGSV")) {
systemType = "BeiDou";
} else if (gsv.startsWith("$GNGSA")) { } else if (gsv.startsWith("$GNGSA")) {
systemType = "GNSS"; systemType = "GNSS";
} }
@@ -448,6 +531,10 @@ public class NMEAParser {
case "Galileo": case "Galileo":
galileoSatellites = satellitesInView; galileoSatellites = satellitesInView;
break; 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) * Парсит GSA сообщение (GPS DOP and Active Satellites)
* КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников * КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников
@@ -544,15 +670,18 @@ public class NMEAParser {
private void parseGSA(String gsa) { private void parseGSA(String gsa) {
Log.d(TAG, "Парсим GSA: " + gsa); Log.d(TAG, "Парсим GSA: " + gsa);
Matcher matcher = GSA_PATTERN.matcher(gsa); Matcher matcher = GSA_PATTERN.matcher(gsa);
Matcher truncatedMatcher = GSA_TRUNCATED_PATTERN.matcher(gsa);
if (matcher.matches()) { if (matcher.matches()) {
// Log.d(TAG, "GSA совпадает с паттерном"); Log.d(TAG, "GSA совпадает с паттерном");
// Подсчитываем активные спутники (непустые поля) // Подсчитываем активные спутники (непустые поля)
int activeSatellites = 0; 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); String satId = matcher.group(i);
if (satId != null && !satId.trim().isEmpty() && !satId.equals("0")) { if (satId != null && !satId.trim().isEmpty() && !satId.equals("0")) {
activeSatellites++; activeSatellites++;
Log.d(TAG, "Активный спутник: " + satId);
} }
} }
@@ -561,35 +690,35 @@ public class NMEAParser {
double hdop = 0.0; double hdop = 0.0;
double vdop = 0.0; double vdop = 0.0;
String pdopStr = matcher.group(14); String pdopStr = matcher.group(15); // PDOP в группе 15
if (pdopStr != null && !pdopStr.trim().isEmpty()) { if (pdopStr != null && !pdopStr.trim().isEmpty()) {
try { try {
pdop = Double.parseDouble(pdopStr); pdop = Double.parseDouble(pdopStr);
} catch (NumberFormatException e) { } 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()) { if (hdopStr != null && !hdopStr.trim().isEmpty()) {
try { try {
hdop = Double.parseDouble(hdopStr); hdop = Double.parseDouble(hdopStr);
} catch (NumberFormatException e) { } 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()) { if (vdopStr != null && !vdopStr.trim().isEmpty()) {
try { try {
vdop = Double.parseDouble(vdopStr); vdop = Double.parseDouble(vdopStr);
} catch (NumberFormatException e) { } 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", Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f",
// activeSatellites, pdop, hdop, vdop)); activeSatellites, pdop, hdop, vdop));
// Обновляем информацию о спутниках // Обновляем информацию о спутниках
ownVessel.setActiveSatellites(activeSatellites); ownVessel.setActiveSatellites(activeSatellites);
@@ -604,13 +733,59 @@ public class NMEAParser {
gpsLocationListener.setSatellitesInVessel(ownVessel); 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 // Уведомляем слушателя о DOP
if (listener != null) { if (listener != null) {
listener.onDOPUpdated(pdop, hdop, vdop); listener.onDOPUpdated(pdop, hdop, vdop);
listener.onVesselUpdated(ownVessel); listener.onVesselUpdated(ownVessel);
} }
} else { } 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; protected OnDockResizeListener dockResizeListener;
// Интерфейс для уведомления об изменении состояния docked
public interface OnDockStateChangeListener {
void onDockStateChanged(boolean isDocked, boolean isTop);
}
protected OnDockStateChangeListener dockStateChangeListener;
public BaseDockWidget(Context context) { public BaseDockWidget(Context context) {
super(context); super(context);
init(); init();
@@ -246,12 +252,23 @@ public abstract class BaseDockWidget extends FrameLayout {
dockHeightPx = newHeight; dockHeightPx = newHeight;
setLayoutParams(lp); setLayoutParams(lp);
// Если закреплен снизу, нужно также изменить позицию Y // Корректируем позицию Y в зависимости от позиции закрепления
if (!dockTop) { if (dockTop) {
// Если закреплен сверху, позиция Y всегда должна быть 0
setY(0);
} else {
// Если закреплен снизу, позиция Y должна быть (parentHeight - newHeight)
float newY = ((ViewGroup) getParent()).getHeight() - newHeight; float newY = ((ViewGroup) getParent()).getHeight() - newHeight;
setY(newY); setY(newY);
} }
// Перепозиционируем все docked виджеты после изменения размера
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
repositionAllDockedWidgets(parent);
}
// Уведомляем об изменении размера
if (dockResizeListener != null) { if (dockResizeListener != null) {
dockResizeListener.onDockResize(newHeight); dockResizeListener.onDockResize(newHeight);
} }
@@ -362,9 +379,13 @@ public abstract class BaseDockWidget extends FrameLayout {
float endX = targetX; float endX = targetX;
float endY = targetY; float endY = targetY;
// Если доким в нижнюю часть, корректируем позицию Y // Если доким, вычисляем правильную позицию с учетом других docked виджетов
if (docked && !top) { if (docked) {
endY = parentHeight - dockHeight; endX = 0;
endY = calculateDockPosition(top);
} else {
// При переходе в movable режим сбрасываем размер до дефолтного
dockHeightPx = 0;
} }
// Сохраняем финальные значения для использования в lambda и inner class // Сохраняем финальные значения для использования в lambda и inner class
@@ -414,6 +435,11 @@ public abstract class BaseDockWidget extends FrameLayout {
morphAnimator.start(); morphAnimator.start();
this.isDocked = docked; this.isDocked = docked;
isMorphing = true; isMorphing = true;
// Уведомляем об изменении состояния docked
if (dockStateChangeListener != null) {
dockStateChangeListener.onDockStateChanged(docked, top);
}
} }
public boolean isDocked() { public boolean isDocked() {
@@ -432,10 +458,101 @@ public abstract class BaseDockWidget extends FrameLayout {
this.dockResizeListener = listener; this.dockResizeListener = listener;
} }
public void setOnDockStateChangeListener(OnDockStateChangeListener listener) {
this.dockStateChangeListener = listener;
}
protected float dp(float dp) { protected float dp(float dp) {
return dp * getResources().getDisplayMetrics().density; 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 onDrawDock(Canvas canvas);
protected abstract void onDrawCircle(Canvas canvas); protected abstract void onDrawCircle(Canvas canvas);
@@ -80,7 +80,7 @@ public class CompassView extends BaseDockWidget {
// Прямая шкала (dock-режим) // Прямая шкала (dock-режим)
@Override @Override
protected void onDrawDock(Canvas canvas) { 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 w = getWidth();
float h = getHeight(); float h = getHeight();
@@ -192,13 +192,13 @@ public class CompassView extends BaseDockWidget {
// Круглый компас (draggable-режим) // Круглый компас (draggable-режим)
@Override @Override
protected void onDrawCircle(Canvas canvas) { 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 w = getWidth();
float h = getHeight(); float h = getHeight();
if (w <= 0 || h <= 0) { if (w <= 0 || h <= 0) {
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h); // Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
return; 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);
}
}
+32 -7
View File
@@ -12,13 +12,15 @@
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<!-- Панель управления --> <!-- Панель управления -->
<!-- android:layout_below="@id/compass_view"-->
<LinearLayout <LinearLayout
android:id="@+id/control_panel"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_margin="16dp" android:layout_margin="16dp"
android:background="@android:color/white" android:background="@android:color/transparent"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp" android:padding="8dp"
android:elevation="4dp"> android:elevation="4dp">
@@ -29,16 +31,28 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Центр на судне" android:text="Центр на судне"
android:textSize="12sp" android:textSize="12sp"
android:minWidth="100dp" /> android:minWidth="120dp"
android:background="@android:color/white"
android:layout_marginBottom="8dp" />
<Button <Button
android:id="@+id/btn_test_compass" android:id="@+id/btn_map_orientation"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Тест компаса" android:text="Карта по северу"
android:textSize="12sp" android:textSize="12sp"
android:minWidth="100dp" android:minWidth="120dp"
android:layout_marginTop="8dp" /> android:background="@android:color/white"
android:layout_marginBottom="8dp" />
<Button
android:id="@+id/btn_settings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="⚙️ Настройки"
android:textSize="12sp"
android:minWidth="120dp"
android:background="@android:color/white" />
</LinearLayout> </LinearLayout>
@@ -53,6 +67,17 @@
android:layout_marginRight="0dp" android:layout_marginRight="0dp"
android:layout_marginBottom="0dp" /> android:layout_marginBottom="0dp" />
<!-- Виджет координат -->
<com.grigowashere.aismap.view.CoordinatesDockWidget
android:id="@+id/coordinates_widget"
android:layout_width="match_parent"
android:layout_height="80dp"
android:layout_alignParentBottom="true"
android:layout_marginLeft="0dp"
android:layout_marginTop="0dp"
android:layout_marginRight="0dp"
android:layout_marginBottom="0dp" />
<!-- Простая информационная панель <!-- Простая информационная панель
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -0,0 +1,238 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<!-- Заголовок -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="⚙️ Настройки AIS Map"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:gravity="center"
android:layout_marginBottom="24dp" />
<!-- UDP Настройки -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📡 UDP Настройки"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_marginBottom="12dp" />
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:hint="UDP Порт"
app:helperText="Порт для прослушивания AIS данных">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_udp_port"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"
android:text="10110" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_udp_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Включить UDP слушатель"
android:textSize="16sp"
android:checked="true" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Приоритеты данных -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardCornerRadius="8dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="📊 Приоритеты данных"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_marginBottom="12dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Выберите источники данных для получения координат и навигационной информации:"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_android_nmea_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Android NMEA (GPS API)"
android:textSize="16sp"
android:checked="true"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Использовать встроенный GPS Android для получения координат"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switch_udp_nmea_enabled"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="UDP NMEA"
android:textSize="16sp"
android:checked="true"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Получать NMEA данные через UDP (курс, скорость, спутники)"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="16dp"
android:layout_marginStart="16dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Режим работы:"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@android:color/black"
android:layout_marginBottom="8dp" />
<RadioGroup
android:id="@+id/radio_group_data_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radio_hybrid_mode"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Гибридный режим (рекомендуется)"
android:textSize="14sp"
android:checked="true" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Координаты от Android GPS, остальное от NMEA"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="8dp"
android:layout_marginStart="16dp" />
<RadioButton
android:id="@+id/radio_nmea_only"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Только NMEA"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Все данные только из NMEA сообщений"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="8dp"
android:layout_marginStart="16dp" />
<RadioButton
android:id="@+id/radio_android_only"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Только Android GPS"
android:textSize="14sp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Только встроенный GPS Android"
android:textSize="12sp"
android:textColor="@android:color/darker_gray"
android:layout_marginBottom="8dp"
android:layout_marginStart="16dp" />
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- Кнопки -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<Button
android:id="@+id/btn_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Отмена"
android:layout_marginEnd="8dp"
style="@style/Widget.Material3.Button.OutlinedButton" />
<Button
android:id="@+id/btn_save"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Сохранить"
style="@style/Widget.Material3.Button" />
</LinearLayout>
</LinearLayout>
</ScrollView>
File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+71
View File
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 232.51 232.77">
<defs>
<clipPath id="clippath">
<polygon points="153.63 0 153.63 0 232.51 0 232.51 232.77 153.63 232.77 153.63 0" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-1">
<polygon points="153.63 0 153.63 0 232.51 0 232.51 79.69 153.63 79.69 153.63 0" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-2">
<polygon points="153.63 232.77 153.63 232.77 232.51 232.77 232.51 153.08 153.63 153.08 153.63 232.77" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-3">
<polygon points="78.87 0 78.87 0 0 0 0 232.77 78.87 232.77 78.87 0" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-4">
<polygon points="0 79.69 0 79.69 78.87 79.69 78.87 0 0 0 0 79.69" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-5">
<polygon points="0 153.08 0 153.08 78.87 153.08 78.87 232.77 0 232.77 0 153.08" style="fill: none;"/>
</clipPath>
</defs>
<g id="_Слой_4" data-name="Слой_4">
<g>
<g id="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath);">
<g>
<g id="_x3C_Зеркальный_повтор_x3E_-2" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-1);">
<g>
<line x1="153.63" y1="4.5" x2="232.51" y2="4.5" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="227.77" y1=".82" x2="227.77" y2="79.69" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
<g id="_x3C_Зеркальный_повтор_x3E_-3" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-2);">
<g>
<line x1="153.63" y1="228.27" x2="232.51" y2="228.27" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="227.77" y1="231.95" x2="227.77" y2="153.08" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="_x3C_Зеркальный_повтор_x3E_-4" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-3);">
<g>
<g id="_x3C_Зеркальный_повтор_x3E_-5" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-4);">
<g>
<line x1="78.87" y1="4.5" x2="0" y2="4.5" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="4.73" y1=".82" x2="4.73" y2="79.69" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
<g id="_x3C_Зеркальный_повтор_x3E_-6" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-5);">
<g>
<line x1="78.87" y1="228.27" x2="0" y2="228.27" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="4.73" y1="231.95" x2="4.73" y2="153.08" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 239.36 239.36">
<g id="_Слой_6" data-name="Слой_6">
<line x1="3.18" y1="3.18" x2="236.18" y2="236.18" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="236.18" y1="3.18" x2="3.18" y2="236.18" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 471 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 165.04 282">
<g id="_Слой_5" data-name="Слой_5">
<polygon points="120.41 4.5 43.14 4.5 4.5 92.81 4.5 277.5 160.5 277.5 159.04 92.81 120.41 4.5" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 381 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 91.38 162.6">
<g id="_Слой_2-2" data-name="Слой_2">
<polygon points="45.69 16.63 5.94 158.1 85.44 158.1 45.69 16.63" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 232.51 232.77">
<defs>
<clipPath id="clippath">
<polygon points="153.63 0 153.63 0 232.51 0 232.51 232.77 153.63 232.77 153.63 0" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-1">
<polygon points="153.63 0 153.63 0 232.51 0 232.51 79.69 153.63 79.69 153.63 0" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-2">
<polygon points="153.63 232.77 153.63 232.77 232.51 232.77 232.51 153.08 153.63 153.08 153.63 232.77" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-3">
<polygon points="78.87 0 78.87 0 0 0 0 232.77 78.87 232.77 78.87 0" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-4">
<polygon points="0 79.69 0 79.69 78.87 79.69 78.87 0 0 0 0 79.69" style="fill: none;"/>
</clipPath>
<clipPath id="clippath-5">
<polygon points="0 153.08 0 153.08 78.87 153.08 78.87 232.77 0 232.77 0 153.08" style="fill: none;"/>
</clipPath>
</defs>
<g id="_Слой_4" data-name="Слой_4">
<g>
<g id="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath);">
<g>
<g id="_x3C_Зеркальный_повтор_x3E_-2" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-1);">
<g>
<line x1="153.63" y1="4.5" x2="232.51" y2="4.5" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="227.77" y1=".82" x2="227.77" y2="79.69" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
<g id="_x3C_Зеркальный_повтор_x3E_-3" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-2);">
<g>
<line x1="153.63" y1="228.27" x2="232.51" y2="228.27" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="227.77" y1="231.95" x2="227.77" y2="153.08" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
</g>
</g>
</g>
<g id="_x3C_Зеркальный_повтор_x3E_-4" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-3);">
<g>
<g id="_x3C_Зеркальный_повтор_x3E_-5" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-4);">
<g>
<line x1="78.87" y1="4.5" x2="0" y2="4.5" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="4.73" y1=".82" x2="4.73" y2="79.69" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
<g id="_x3C_Зеркальный_повтор_x3E_-6" data-name="_x3C_Зеркальный_повтор_x3E_">
<g style="clip-path: url(#clippath-5);">
<g>
<line x1="78.87" y1="228.27" x2="0" y2="228.27" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="4.73" y1="231.95" x2="4.73" y2="153.08" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 239.36 239.36">
<g id="_Слой_6" data-name="Слой_6">
<line x1="3.18" y1="3.18" x2="236.18" y2="236.18" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
<line x1="236.18" y1="3.18" x2="3.18" y2="236.18" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 471 B

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 165.04 282">
<g id="_Слой_5" data-name="Слой_5">
<polygon points="120.41 4.5 43.14 4.5 4.5 92.81 4.5 277.5 160.5 277.5 159.04 92.81 120.41 4.5" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 381 B

+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 91.38 162.6">
<g id="_Слой_2-2" data-name="Слой_2">
<polygon points="45.69 16.63 5.94 158.1 85.44 158.1 45.69 16.63" style="fill: none; stroke: #000; stroke-miterlimit: 10; stroke-width: 9px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 354 B