generated from Grigo/AndroidTemplate
Подготовка к крупным изменениям: карта, 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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user