Подготовка к крупным изменениям: карта, AIS и UI

- Яндекс/MapForge: правки в менеджерах и обёртках маркеров (улучшена отрисовка/логика)
- NMEAParser: корректировки парсинга и стабильности
- Модель AISVessel: уточнение полей/логики
- Настройки: правки в SettingsActivity и SettingsManager, актуализация AppController
- UI: обновлены activity_main, activity_settings, bottom_sheet_ais_vessel; меню main_menu
- Ресурсы: добавлен drawable/targetclassa.xml, обновлён drawable/target.xml
- Конфигурация: правки AndroidManifest и app/build.gradle
- Прочее: изменения в .idea (не влияют на сборку)
This commit is contained in:
2025-09-23 11:53:23 +03:00
parent a2f1775f9f
commit 41432665ea
37 changed files with 6561 additions and 161 deletions
@@ -14,6 +14,7 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import android.view.ViewGroup;
import android.graphics.Color;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
@@ -31,6 +32,7 @@ import com.grigowashere.aismap.view.CoordinatesDockWidget;
import com.grigowashere.aismap.view.BaseDockWidget;
import com.grigowashere.aismap.utils.SettingsManager;
import com.grigowashere.aismap.utils.LogSender;
import com.grigowashere.aismap.utils.MIDToCountry;
import com.yandex.mapkit.mapview.MapView;
import java.util.List;
import java.util.ArrayList;
@@ -53,10 +55,15 @@ public class MainActivity extends AppCompatActivity {
private Button btnCenterOnVessel;
private Button btnMapOrientation;
private Button btnSettings;
private Button btnAisTargets;
private LinearLayout controlPanel;
private CompassView compassView;
private CompassSensor compassSensor;
private CoordinatesDockWidget coordinatesWidget;
private TextView tvGpsAge;
private TextView tvAisAge;
private android.os.Handler messageAgeHandler;
private Runnable messageAgeRunnable;
// BottomSheet для отображения информации о нашем судне
private BottomSheetDialog ownVesselBottomSheet;
@@ -73,6 +80,10 @@ public class MainActivity extends AppCompatActivity {
private android.os.Handler bottomSheetUpdateHandler; // Handler для обновления BottomSheet
private Runnable bottomSheetUpdateRunnable; // Runnable для обновления BottomSheet
private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000; // Обновление каждую секунду
// Отложенное центрирование из внешнего интента
private Double pendingCenterLat = null;
private Double pendingCenterLon = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -93,9 +104,12 @@ public class MainActivity extends AppCompatActivity {
btnCenterOnVessel = findViewById(R.id.btn_center_vessel);
btnMapOrientation = findViewById(R.id.btn_map_orientation);
btnSettings = findViewById(R.id.btn_settings);
btnAisTargets = findViewById(R.id.btn_ais_targets);
controlPanel = findViewById(R.id.control_panel);
compassView = findViewById(R.id.compass_view);
coordinatesWidget = findViewById(R.id.coordinates_widget);
tvGpsAge = findViewById(R.id.tv_gps_age);
tvAisAge = findViewById(R.id.tv_ais_age);
// Инициализируем магнитный компас
compassSensor = new CompassSensor(this);
@@ -104,12 +118,16 @@ public class MainActivity extends AppCompatActivity {
setupButtonListeners();
setupCompass();
setupCoordinatesWidget();
setupMessageAgesUpdater();
}
private void setupButtonListeners() {
btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
btnSettings.setOnClickListener(v -> showSettings());
if (btnAisTargets != null) {
btnAisTargets.setOnClickListener(v -> openAisTargets());
}
// Кнопка для показа информации о судне
// Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info);
@@ -216,6 +234,46 @@ public class MainActivity extends AppCompatActivity {
updateControlPanelPosition();
});
}
private void setupMessageAgesUpdater() {
messageAgeHandler = new android.os.Handler(android.os.Looper.getMainLooper());
messageAgeRunnable = new Runnable() {
@Override
public void run() {
try {
if (appController != null) {
int gpsSec = appController.getSecondsSinceLastGPSMessage();
int aisSec = appController.getSecondsSinceLastAISMessage();
if (tvGpsAge != null) {
tvGpsAge.setText(gpsSec >= 0 ? ("GPS: " + gpsSec + " сек назад") : "GPS: --");
tvGpsAge.setTextColor(getAgeColor(gpsSec));
}
if (tvAisAge != null) {
tvAisAge.setText(aisSec >= 0 ? ("AIS: " + aisSec + " сек назад") : "AIS: --");
tvAisAge.setTextColor(getAgeColor(aisSec));
}
}
} catch (Exception ignored) {}
messageAgeHandler.postDelayed(this, 1000);
}
};
// Стартуем после первичной инициализации
messageAgeHandler.postDelayed(messageAgeRunnable, 1000);
}
private int getAgeColor(int seconds) {
if (seconds < 0) {
// Нет данных
return Color.parseColor("#F44336"); // красный
}
if (seconds < 30) {
return Color.parseColor("#4CAF50"); // зелёный
} else if (seconds < 300) {
return Color.parseColor("#FFC107"); // жёлтый
} else {
return Color.parseColor("#F44336"); // красный
}
}
private void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels) {
if (compassView != null) {
@@ -329,6 +387,18 @@ public class MainActivity extends AppCompatActivity {
mapController = new MapController(this);
// Устанавливаем callback для обновления UI
// Запускаем Foreground Service для фоновых обновлений AIS/GPS
try {
android.content.Intent svc = new android.content.Intent(this, com.grigowashere.aismap.services.AISForegroundService.class);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
startForegroundService(svc);
} else {
startService(svc);
}
} catch (Exception e) {
android.util.Log.e("MainActivity", "Не удалось запустить ForegroundService: " + e.getMessage(), e);
}
appController.setUIUpdateCallback(new AppController.ExtendedUIUpdateCallback() {
@Override
public void onVesselPositionUpdated(Vessel vessel) {
@@ -470,20 +540,45 @@ public class MainActivity extends AppCompatActivity {
Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show();
}
private void togglePathTracking() {
boolean currentState = settingsManager.isPathTrackingEnabled();
boolean newState = !currentState;
settingsManager.setPathTrackingEnabled(newState);
// Обновляем состояние в карте
if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) {
((com.grigowashere.aismap.maps.YandexMapImpl) mapInterface).setPathTrackingEnabled(newState);
}
String message = newState ? "Отслеживание путей включено" : "Отслеживание путей выключено";
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
// Обновляем меню
invalidateOptionsMenu();
}
private void showSettings() {
Intent intent = new Intent(this, SettingsActivity.class);
startActivityForResult(intent, SETTINGS_REQUEST_CODE);
}
private void openAisTargets() {
Intent intent = new Intent(this, AisTargetsActivity.class);
startActivity(intent);
}
/**
* Обновляет позицию панели управления в зависимости от состояния docked виджетов
*/
private void updateControlPanelPosition() {
if (controlPanel != null) {
runOnUiThread(() -> {
// Получаем текущие параметры layout
android.widget.RelativeLayout.LayoutParams params =
(android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams();
// Используем postDelayed для предотвращения частых обновлений layout
controlPanel.postDelayed(() -> {
try {
// Получаем текущие параметры layout
android.widget.RelativeLayout.LayoutParams params =
(android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams();
int topMargin = dpToPx(16); // По умолчанию отступ сверху
int bottomMargin = dpToPx(16); // По умолчанию отступ снизу
@@ -525,16 +620,19 @@ public class MainActivity extends AppCompatActivity {
// Применяем новые параметры
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));
});
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));
} catch (Exception e) {
Log.e(TAG, "Ошибка при обновлении позиции панели управления: " + e.getMessage(), e);
}
}, 50); // Задержка 50мс для throttling
}
}
@@ -574,6 +672,16 @@ public class MainActivity extends AppCompatActivity {
mapInterface.initialize();
Log.i(TAG, "Карта инициализирована");
// Применяем отложенное центрирование, если было
applyPendingCenterIfAny();
// Инициализируем отслеживание путей
if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) {
boolean pathTrackingEnabled = settingsManager.isPathTrackingEnabled();
((com.grigowashere.aismap.maps.YandexMapImpl) mapInterface).setPathTrackingEnabled(pathTrackingEnabled);
Log.i(TAG, "Отслеживание путей: " + (pathTrackingEnabled ? "включено" : "выключено"));
}
// Проверяем, что все настроено правильно
Log.i(TAG, "Проверяем настройку карты...");
@@ -590,9 +698,53 @@ public class MainActivity extends AppCompatActivity {
}
}
// Обрабатываем возможный интент центрирования
handleCenterIntentIfAny(getIntent());
// Проверяем разрешения и запускаем контроллеры
checkPermissions();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
handleCenterIntentIfAny(intent);
}
private void handleCenterIntentIfAny(Intent intent) {
if (intent == null) return;
if (intent.hasExtra("center_lat") && intent.hasExtra("center_lon")) {
double lat = intent.getDoubleExtra("center_lat", 0);
double lon = intent.getDoubleExtra("center_lon", 0);
Log.i(TAG, "Получен интент центрирования: lat=" + lat + ", lon=" + lon);
if (lat != 0 || lon != 0) {
if (mapInterface != null) {
Log.i(TAG, "Центрируем карту немедленно");
mapInterface.centerOnPosition(lat, lon);
} else {
// Сохраняем для применения после инициализации карты
Log.i(TAG, "Сохраняем координаты для отложенного центрирования");
pendingCenterLat = lat;
pendingCenterLon = lon;
}
}
// Сбрасываем, чтобы не повторялось при поворотах
intent.removeExtra("center_lat");
intent.removeExtra("center_lon");
intent.removeExtra("center_mmsi");
}
}
private void applyPendingCenterIfAny() {
if (mapInterface == null) return;
if (pendingCenterLat != null && pendingCenterLon != null) {
Log.i(TAG, "Применяем отложенное центрирование: lat=" + pendingCenterLat + ", lon=" + pendingCenterLon);
mapInterface.centerOnPosition(pendingCenterLat, pendingCenterLon);
pendingCenterLat = null;
pendingCenterLon = null;
}
}
@Override
protected void onStop() {
@@ -603,10 +755,10 @@ public class MainActivity extends AppCompatActivity {
mapInterface.cleanup();
}
// Останавливаем все слушатели
if (appController != null) {
appController.stopAllListeners();
}
// Не останавливаем слушатели здесь, чтобы UDP продолжал работать в фоне
// if (appController != null) {
// appController.stopAllListeners();
// }
}
@Override
@@ -727,6 +879,12 @@ public class MainActivity extends AppCompatActivity {
udpItem.setTitle(appController.isUDPEnabled() ? "UDP ✓" : "UDP");
}
MenuItem pathItem = menu.findItem(R.id.menu_path_tracking);
if (pathItem != null) {
boolean pathEnabled = settingsManager.isPathTrackingEnabled();
pathItem.setTitle(pathEnabled ? "Пути ✓" : "Пути");
}
return true;
}
@@ -743,6 +901,9 @@ public class MainActivity extends AppCompatActivity {
} else if (id == R.id.menu_clear_ais) {
clearAIS();
return true;
} else if (id == R.id.menu_path_tracking) {
togglePathTracking();
return true;
}
return super.onOptionsItemSelected(item);
@@ -996,12 +1157,13 @@ public class MainActivity extends AppCompatActivity {
// Обновляем все поля в AIS BottomSheet
TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title);
TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi);
TextView tvName = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_name);
TextView tvCallsign = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_callsign);
TextView tvImo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_imo);
TextView tvType = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_type);
TextView tvPosition = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_position);
TextView tvCourse = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_course);
TextView tvRot = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_rot);
TextView tvHeading = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_heading);
TextView tvSpeed = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_speed);
TextView tvDimensions = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_dimensions);
TextView tvDraft = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_draft);
@@ -1015,9 +1177,11 @@ public class MainActivity extends AppCompatActivity {
// Заголовок
if (tvTitle != null) {
String title = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty()
? "🚢 " + vessel.getVesselName()
: "🚢 AIS СУДНО";
String name = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty()
? vessel.getVesselName()
: "AIS СУДНО";
String flag = getFlagEmojiForMMSI(vessel.getMmsi());
String title = (flag != null ? flag + " " : "") + "🚢 " + name;
tvTitle.setText(title);
}
@@ -1027,9 +1191,7 @@ public class MainActivity extends AppCompatActivity {
}
// Название судна
if (tvName != null) {
tvName.setText("📛 Название: " + (vessel.getVesselName() != null ? vessel.getVesselName() : "--"));
}
// Позывной
if (tvCallsign != null) {
@@ -1057,13 +1219,34 @@ public class MainActivity extends AppCompatActivity {
}
}
// Курс
// Курс (COG)
if (tvCourse != null) {
if (vessel.getCourse() > 0) {
String courseText = String.format("🧭 Курс: %.1f°", vessel.getCourse());
String courseText = String.format("🧭 COG: %.1f°", vessel.getCourse());
tvCourse.setText(courseText);
} else {
tvCourse.setText("🧭 Курс: --°");
tvCourse.setText("🧭 COG: --°");
}
}
// Скорость поворота (ROT)
if (tvRot != null) {
double rot = vessel.getRateOfTurn();
if (rot != 0) {
String rotText = String.format("🔄 ROT: %.1f°/мин", rot);
tvRot.setText(rotText);
} else {
tvRot.setText("🔄 ROT: --°/мин");
}
}
// Направление (HDG)
if (tvHeading != null) {
if (vessel.getHeading() > 0) {
String headingText = String.format("🧭 HDG: %.1f°", vessel.getHeading());
tvHeading.setText(headingText);
} else {
tvHeading.setText("🧭 HDG: --°");
}
}
@@ -1130,7 +1313,9 @@ public class MainActivity extends AppCompatActivity {
String signalText = String.format("📶 Сигнал: %d", vessel.getSignalStrength());
tvSignal.setText(signalText);
} else {
tvSignal.setText("📶 Сигнал: --");
// Показываем качество позиции по AIS Accuracy биту
String qualityText = vessel.isPositionAccuracy() ? "📶 Точность: высокая" : "📶 Точность: низкая";
tvSignal.setText(qualityText);
}
}
@@ -1175,6 +1360,26 @@ public class MainActivity extends AppCompatActivity {
return days + " дн";
}
}
/**
* Возвращает флаг-эмодзи по MMSI через MID->ISO2.
*/
private String getFlagEmojiForMMSI(String mmsi) {
try {
if (mmsi == null || mmsi.length() < 3) return null;
String mid = mmsi.substring(0, 3);
String iso2 = MIDToCountry.MID_TO_COUNTRY.get(mid);
if (iso2 == null || iso2.length() != 2) return null;
char a = Character.toUpperCase(iso2.charAt(0));
char b = Character.toUpperCase(iso2.charAt(1));
int base = 0x1F1E6;
int cp1 = base + (a - 'A');
int cp2 = base + (b - 'A');
return new String(Character.toChars(cp1)) + new String(Character.toChars(cp2));
} catch (Exception ignored) {
return null;
}
}
/**
* Восстанавливает обработчики кликов для маркеров
@@ -1226,7 +1431,7 @@ public class MainActivity extends AppCompatActivity {
if (!isYandexMapsInitialized) {
try {
// Инициализация Яндекс.Карт
com.yandex.mapkit.MapKitFactory.setApiKey("your_api_key_here");
com.yandex.mapkit.MapKitFactory.setApiKey("9ae1917c-2049-4927-9d1e-29dd0d3e8ebc");
com.yandex.mapkit.MapKitFactory.initialize(this);
isYandexMapsInitialized = true;