package com.grigowashere.aismap.ui; import android.content.Context; import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.widget.TextView; import com.google.android.material.bottomsheet.BottomSheetDialog; import com.grigowashere.aismap.R; import com.grigowashere.aismap.controllers.AppCoordinator; import com.grigowashere.aismap.models.AISVessel; import com.grigowashere.aismap.models.Vessel; /** * Полный менеджер BottomSheet-ов: создание, показ, обновление и авто-обновление. * Делегирует всю прежнюю логику из MainActivity, чтобы разгрузить активити. */ public class BottomSheetsManager { private static final String TAG = "BottomSheetsManager"; private final Context context; private final AppCoordinator appCoordinator; private BottomSheetDialog ownVesselBottomSheet; private View bottomSheetView; private BottomSheetDialog aisVesselBottomSheet; private View aisBottomSheetView; private AISVessel currentAISVessel; private android.os.Handler timeUpdateHandler; private Runnable timeUpdateRunnable; private android.os.Handler bottomSheetUpdateHandler; private Runnable bottomSheetUpdateRunnable; private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000; public BottomSheetsManager(Context context, AppCoordinator appCoordinator) { this.context = context; this.appCoordinator = appCoordinator; } public void init() { // Время timeUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); timeUpdateRunnable = new Runnable() { @Override public void run() { if (currentAISVessel != null && aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing()) { updateAISTimeAgo(); } timeUpdateHandler.postDelayed(this, 1000); } }; // Авто-обновление контента bottomSheetUpdateHandler = new android.os.Handler(android.os.Looper.getMainLooper()); bottomSheetUpdateRunnable = new Runnable() { @Override public void run() { if (ownVesselBottomSheet != null && ownVesselBottomSheet.isShowing()) { updateOwnVesselUI(); } if (aisVesselBottomSheet != null && aisVesselBottomSheet.isShowing() && currentAISVessel != null) { updateAISBottomSheetUI(currentAISVessel); } bottomSheetUpdateHandler.postDelayed(this, BOTTOM_SHEET_UPDATE_INTERVAL); } }; // Наше судно ownVesselBottomSheet = new BottomSheetDialog(context); bottomSheetView = android.view.LayoutInflater.from(context).inflate(R.layout.bottom_sheet_own_vessel, null); ownVesselBottomSheet.setContentView(bottomSheetView); ImageButton btnClose = bottomSheetView.findViewById(R.id.btn_close_bottom_sheet); if (btnClose != null) { btnClose.setOnClickListener(v -> { ownVesselBottomSheet.dismiss(); stopBottomSheetAutoUpdate(); }); } ownVesselBottomSheet.setCanceledOnTouchOutside(true); ownVesselBottomSheet.setCancelable(true); ownVesselBottomSheet.setOnDismissListener(dialog -> stopBottomSheetAutoUpdate()); // AIS судно aisVesselBottomSheet = new BottomSheetDialog(context); aisBottomSheetView = android.view.LayoutInflater.from(context).inflate(R.layout.bottom_sheet_ais_vessel, null); aisVesselBottomSheet.setContentView(aisBottomSheetView); ImageButton btnCloseAIS = aisBottomSheetView.findViewById(R.id.btn_close_ais_bottom_sheet); if (btnCloseAIS != null) { btnCloseAIS.setOnClickListener(v -> { aisVesselBottomSheet.dismiss(); stopTimeUpdate(); stopBottomSheetAutoUpdate(); }); } aisVesselBottomSheet.setCanceledOnTouchOutside(true); aisVesselBottomSheet.setCancelable(true); aisVesselBottomSheet.setOnDismissListener(dialog -> { stopTimeUpdate(); stopBottomSheetAutoUpdate(); }); } public void showOwnVessel() { if (ownVesselBottomSheet != null && !ownVesselBottomSheet.isShowing()) { updateOwnVesselUI(); ownVesselBottomSheet.show(); startBottomSheetAutoUpdate(); } } public void showAISVessel(AISVessel vessel) { if (aisVesselBottomSheet != null && !aisVesselBottomSheet.isShowing()) { currentAISVessel = vessel; updateAISBottomSheetUI(vessel); aisVesselBottomSheet.show(); startTimeUpdate(); startBottomSheetAutoUpdate(); } } public void updateOwnVesselUI() { if (bottomSheetView == null) return; Vessel vessel = appCoordinator.getOwnVessel(); if (vessel == null) return; TextView tvStatus = bottomSheetView.findViewById(R.id.bottom_sheet_status); TextView tvPosition = bottomSheetView.findViewById(R.id.bottom_sheet_position); TextView tvCourse = bottomSheetView.findViewById(R.id.bottom_sheet_course); TextView tvSpeed = bottomSheetView.findViewById(R.id.bottom_sheet_speed); TextView tvAltitude = bottomSheetView.findViewById(R.id.bottom_sheet_altitude); TextView tvAccuracy = bottomSheetView.findViewById(R.id.bottom_sheet_accuracy); TextView tvGPSQuality = bottomSheetView.findViewById(R.id.bottom_sheet_gps_quality); TextView tvSatellites = bottomSheetView.findViewById(R.id.bottom_sheet_satellites); TextView tvDOP = bottomSheetView.findViewById(R.id.bottom_sheet_dop); TextView tvFixTime = bottomSheetView.findViewById(R.id.bottom_sheet_fix_time); TextView tvFixQuality = bottomSheetView.findViewById(R.id.bottom_sheet_fix_quality); if (tvStatus != null) tvStatus.setText((vessel.getLatitude()!=0 && vessel.getLongitude()!=0) ? "Статус: GPS активен, данные получены" : "Статус: Ожидание GPS данных..."); if (tvPosition != null) tvPosition.setText((vessel.getLatitude()!=0 && vessel.getLongitude()!=0) ? String.format("📍 Координаты: %.6f, %.6f", vessel.getLatitude(), vessel.getLongitude()) : "📍 Координаты: Не определены"); if (tvCourse != null) tvCourse.setText((vessel.getCourse()>0) ? String.format("🧭 Курс: %.1f°", vessel.getCourse()) : "🧭 Курс: --°"); if (tvSpeed != null) tvSpeed.setText((vessel.getSpeed()>0) ? String.format("⚡ Скорость: %.1f узлов", vessel.getSpeed()) : "⚡ Скорость: -- узлов"); if (tvAltitude != null) tvAltitude.setText((vessel.getAltitude()!=0) ? String.format("🏔️ Высота: %.1f м", vessel.getAltitude()) : "🏔️ Высота: -- м"); if (tvAccuracy != null) tvAccuracy.setText((vessel.getAccuracy()>0) ? String.format("🎯 Точность: %.1f м", vessel.getAccuracy()) : "🎯 Точность: -- м"); if (tvGPSQuality != null) tvGPSQuality.setText((vessel.getGPSQualityDescription()!=null) ? String.format("📊 Качество GPS: %s", vessel.getGPSQualityDescription()) : "📊 Качество GPS: --"); if (tvSatellites != null) tvSatellites.setText((vessel.getSatellites()>0) ? String.format("Спутники: %d/%d", vessel.getActiveSatellites(), vessel.getSatellites()) : "Спутники: --/--"); if (tvDOP != null) tvDOP.setText((vessel.getPdop()>0) ? String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", vessel.getPdop(), vessel.getHdop(), vessel.getVdop()) : "📈 DOP: PDOP=-- HDOP=-- VDOP=--"); if (tvFixTime != null) { String t = vessel.getFixTimeNmea(); if (t != null && !t.isEmpty()) { String pretty = t; int dot = pretty.indexOf('.'); if (dot > 0) pretty = pretty.substring(0, dot); if (pretty.length() == 6) { pretty = pretty.substring(0,2) + ":" + pretty.substring(2,4) + ":" + pretty.substring(4,6); } tvFixTime.setText(String.format("🕐 Время фикса: %s", pretty)); } else if (vessel.getFixTime() > 0) { tvFixTime.setText(String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(new java.util.Date(vessel.getFixTime())))); } else { tvFixTime.setText("🕐 Время фикса: --"); } } if (tvFixQuality != null) tvFixQuality.setText((vessel.getFixQuality()!=null) ? String.format("🔒 Качество фикса: %s", vessel.getFixQuality()) : "🔒 Качество фикса: --"); } public void updateAISBottomSheetUI(AISVessel vessel) { if (aisBottomSheetView == null || vessel == null) return; if (currentAISVessel != null && currentAISVessel.getMmsi() != null && currentAISVessel.getMmsi().equals(vessel.getMmsi())) { currentAISVessel = vessel; } TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title); TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi); 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); TextView tvDestination = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_destination); TextView tvEta = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_eta); TextView tvNavStatus = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_nav_status); TextView tvClass = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_class); TextView tvSignal = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_signal); TextView tvDistance = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_distance); TextView tvBearing = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_bearing); TextView tvCpa = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_cpa); TextView tvLastUpdate = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_last_update); TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago); if (tvTitle != null) { String name = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty() ? vessel.getVesselName() : "AIS СУДНО"; // Флаг страны по MMSI оставляем — это единственный визуальный // маркер, который тут реально несёт смысл. Остальные эмодзи в // карточке цели убраны, чтобы текст не выглядел как чат. String flag = getFlagEmojiForMMSI(vessel.getMmsi()); tvTitle.setText((flag != null ? flag + " " : "") + name); } if (tvMmsi != null) tvMmsi.setText("MMSI: " + (vessel.getMmsi() != null ? vessel.getMmsi() : "--")); if (tvCallsign != null) tvCallsign.setText("Позывной: " + (vessel.getCallSign() != null ? vessel.getCallSign() : "--")); if (tvImo != null) tvImo.setText("IMO: " + (vessel.getImo() > 0 ? String.valueOf(vessel.getImo()) : "--")); if (tvType != null) tvType.setText("Тип: " + (vessel.getVesselType() != null ? vessel.getVesselType() : "--")); if (tvPosition != null) { if (vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { tvPosition.setText(String.format("Координаты: %.6f, %.6f", vessel.getLatitude(), vessel.getLongitude())); } else { tvPosition.setText("Координаты: --"); } } if (tvCourse != null) tvCourse.setText(vessel.getCourse() > 0 ? String.format("COG: %.1f°", vessel.getCourse()) : "COG: --°"); if (tvRot != null) tvRot.setText(vessel.getRateOfTurn() != 0 ? String.format("ROT: %.1f°/мин", vessel.getRateOfTurn()) : "ROT: --°/мин"); if (tvHeading != null) tvHeading.setText(vessel.getHeading() > 0 ? String.format("HDG: %.1f°", vessel.getHeading()) : "HDG: --°"); if (tvSpeed != null) tvSpeed.setText(vessel.getSpeed() > 0 ? String.format("Скорость: %.1f узлов", vessel.getSpeed()) : "Скорость: -- узлов"); if (tvDimensions != null) tvDimensions.setText((vessel.getLength() > 0 && vessel.getWidth() > 0) ? String.format("Размеры: %.1f x %.1f м", vessel.getLength(), vessel.getWidth()) : "Размеры: --"); if (tvDraft != null) tvDraft.setText(vessel.getDraft() > 0 ? String.format("Осадка: %.1f м", vessel.getDraft()) : "Осадка: -- м"); if (tvDestination != null) tvDestination.setText("Назначение: " + (vessel.getDestination() != null ? vessel.getDestination() : "--")); if (tvEta != null) tvEta.setText(vessel.getEta() != null ? String.format("ETA: %s", vessel.getEta().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))) : "ETA: --"); if (tvNavStatus != null) tvNavStatus.setText("Статус: " + (vessel.getNavigationalStatus() != null ? vessel.getNavigationalStatus() : "--")); if (tvClass != null) tvClass.setText("Класс: " + (vessel.getVesselClass() != null ? vessel.getVesselClass() : "--")); if (tvSignal != null) { if (vessel.getSignalStrength() > 0) { tvSignal.setText(String.format("Сигнал: %d", vessel.getSignalStrength())); } else { tvSignal.setText(vessel.isPositionAccuracy() ? "Точность: высокая" : "Точность: низкая"); } } if (tvLastUpdate != null) tvLastUpdate.setText(vessel.getLastUpdate() != null ? String.format("Обновлено: %s", vessel.getLastUpdate().format(java.time.format.DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm:ss"))) : "Обновлено: --"); if (tvDistance != null || tvBearing != null || tvCpa != null) { Vessel ourVessel = appCoordinator.getOwnVessel(); String cpaNa = context.getString(R.string.cpa_na); if (ourVessel != null && ourVessel.getLatitude() != 0 && ourVessel.getLongitude() != 0 && vessel.getLatitude() != 0 && vessel.getLongitude() != 0) { double distance = com.grigowashere.aismap.utils.NavigationUtils.calculateDistance( ourVessel.getLatitude(), ourVessel.getLongitude(), vessel.getLatitude(), vessel.getLongitude()); if (tvDistance != null) { tvDistance.setText("Расстояние: " + com.grigowashere.aismap.utils.NavigationUtils.formatDistance(distance)); } double bearing = com.grigowashere.aismap.utils.NavigationUtils.calculateBearing( ourVessel.getLatitude(), ourVessel.getLongitude(), vessel.getLatitude(), vessel.getLongitude()); double relativeBearing = com.grigowashere.aismap.utils.NavigationUtils.calculateRelativeBearing( ourVessel.getCourse(), bearing); if (tvBearing != null) { tvBearing.setText("Пеленг: " + com.grigowashere.aismap.utils.NavigationUtils.formatRelativeBearing(relativeBearing)); } if (tvCpa != null) { com.grigowashere.aismap.utils.SettingsManager sm = new com.grigowashere.aismap.utils.SettingsManager(context); boolean useNm = com.grigowashere.aismap.utils.SettingsManager.RANGE_UNIT_NM .equals(sm.getRangeUnit()); com.grigowashere.aismap.utils.RangeMath.CpaResult cpa = com.grigowashere.aismap.utils.RangeMath.calculateCpa( ourVessel.getLatitude(), ourVessel.getLongitude(), ourVessel.getSpeed(), ourVessel.getCourse(), ourVessel.getHeading(), vessel.getLatitude(), vessel.getLongitude(), vessel.getSpeed(), vessel.getCourse(), vessel.getHeading()); if (cpa.valid) { String cpaDist = com.grigowashere.aismap.utils.RangeMath.formatCpaDistance( cpa.cpaMeters, useNm, java.util.Locale.getDefault()); String tcpa = com.grigowashere.aismap.utils.RangeMath.formatTcpa( cpa.tcpaMinutes, java.util.Locale.getDefault()); tvCpa.setText(context.getString(R.string.bottom_sheet_ais_cpa, cpaDist, tcpa)); } else { tvCpa.setText(context.getString(R.string.bottom_sheet_ais_cpa, cpaNa, cpaNa)); } } } else { if (tvDistance != null) tvDistance.setText("Расстояние: --"); if (tvBearing != null) tvBearing.setText("Пеленг: --"); if (tvCpa != null) tvCpa.setText(context.getString(R.string.bottom_sheet_ais_cpa, cpaNa, cpaNa)); } } if (tvTimeAgo != null) { if (vessel.getLastUpdate() != null) { long secondsAgo = java.time.Duration.between(vessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds(); tvTimeAgo.setText("Время назад: " + formatTimeAgo(secondsAgo)); } else { tvTimeAgo.setText("Время назад: --"); } } } public void startTimeUpdate() { if (timeUpdateHandler != null && timeUpdateRunnable != null) { timeUpdateHandler.postDelayed(timeUpdateRunnable, 1000); } } public void stopTimeUpdate() { if (timeUpdateHandler != null && timeUpdateRunnable != null) { timeUpdateHandler.removeCallbacks(timeUpdateRunnable); } currentAISVessel = null; } public void startBottomSheetAutoUpdate() { if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) { bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable); bottomSheetUpdateHandler.postDelayed(bottomSheetUpdateRunnable, BOTTOM_SHEET_UPDATE_INTERVAL); Log.i(TAG, "Автоматическое обновление BottomSheet запущено"); } } public void stopBottomSheetAutoUpdate() { if (bottomSheetUpdateHandler != null && bottomSheetUpdateRunnable != null) { bottomSheetUpdateHandler.removeCallbacks(bottomSheetUpdateRunnable); Log.i(TAG, "Автоматическое обновление BottomSheet остановлено"); } } public void updateAISTimeAgo() { if (aisBottomSheetView == null || currentAISVessel == null) return; TextView tvTimeAgo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_time_ago); if (tvTimeAgo != null && currentAISVessel.getLastUpdate() != null) { long secondsAgo = java.time.Duration.between(currentAISVessel.getLastUpdate(), java.time.LocalDateTime.now()).getSeconds(); tvTimeAgo.setText("Время назад: " + formatTimeAgo(secondsAgo)); } } public void cleanup() { stopTimeUpdate(); stopBottomSheetAutoUpdate(); try { if (ownVesselBottomSheet != null) ownVesselBottomSheet.dismiss(); } catch (Exception ignore) {} try { if (aisVesselBottomSheet != null) aisVesselBottomSheet.dismiss(); } catch (Exception ignore) {} } private String formatTimeAgo(long seconds) { if (seconds < 60) return seconds + " сек"; if (seconds < 3600) return (seconds / 60) + " мин"; if (seconds < 86400) return (seconds / 3600) + " ч"; return (seconds / 86400) + " дн"; } private String getFlagEmojiForMMSI(String mmsi) { try { if (mmsi == null || mmsi.length() < 3) return null; String mid = mmsi.substring(0, 3); String iso2 = com.grigowashere.aismap.utils.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; } } }