diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
index b268ef3..8d22e48 100644
--- a/.idea/deploymentTargetSelector.xml
+++ b/.idea/deploymentTargetSelector.xml
@@ -4,6 +4,14 @@
+
+
+
+
+
+
+
+
diff --git a/NMEAParser_backup.java b/NMEAParser_backup.java
new file mode 100644
index 0000000..1163a86
--- /dev/null
+++ b/NMEAParser_backup.java
@@ -0,0 +1,2835 @@
+package com.grigowashere.aismap.controllers;
+
+import android.util.Log;
+import com.grigowashere.aismap.models.Vessel;
+import com.grigowashere.aismap.models.AISVessel;
+import com.grigowashere.aismap.utils.LogSender;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Контроллер для парсинга NMEA сообщений
+ * Работает в гибридном режиме: координаты через Location API, остальное через NMEA
+ *
+ * ВАЖНО: Размеры судна в AIS сообщениях рассчитываются относительно положения антенны:
+ * - Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
+ * - Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
+ * Координаты в AIS указывают положение антенны, а не центра судна.
+ */
+
+/**
+ * Контроллер для парсинга NMEA сообщений
+ * Использует простой разбор по запятым вместо регулярных выражений
+ */
+public class NMEAParser {
+
+ private static final String TAG = "NMEAParser";
+
+ private Vessel ownVessel;
+ private List aisVessels;
+ private NMEAParserListener listener;
+ private GPSLocationListener gpsLocationListener;
+
+ // Поля для работы с AIS фрагментами
+ private java.util.Map> aisFragments = new java.util.HashMap<>();
+ private java.util.Map aisFragmentTimestamps = new java.util.HashMap<>();
+ private static final long AIS_FRAGMENT_TIMEOUT = 10000; // 10 секунд
+
+ // Флаг для гибридного режима
+ private boolean hybridMode = true;
+
+ // Поля для отслеживания спутников по системам
+ private int gpsSatellites = 0;
+ private int glonassSatellites = 0;
+ private int galileoSatellites = 0;
+
+ public interface NMEAParserListener {
+ void onVesselUpdated(Vessel vessel);
+ void onAISVesselUpdated(AISVessel vessel);
+ void onParseError(String error);
+ void onDOPUpdated(double pdop, double hdop, double vdop);
+ }
+
+ public NMEAParser() {
+ this.ownVessel = new Vessel();
+ this.aisVessels = new ArrayList<>();
+ }
+
+ public void setListener(NMEAParserListener listener) {
+ this.listener = listener;
+ }
+
+ /**
+ * Устанавливает GPS Location Listener для гибридного режима
+ */
+ public void setGPSLocationListener(GPSLocationListener gpsLocationListener) {
+ this.gpsLocationListener = gpsLocationListener;
+ }
+
+ /**
+ * Включает/выключает гибридный режим
+ */
+ public void setHybridMode(boolean enabled) {
+ this.hybridMode = enabled;
+ Log.i(TAG, "🔄 Гибридный режим: " + (enabled ? "включен" : "отключен"));
+ Log.i(TAG, "📍 В режиме " + (enabled ? "гибридном" : "только NMEA") + " координаты будут " +
+ (enabled ? "браться из Android GPS API" : "браться из NMEA сообщений"));
+ }
+
+ /**
+ * Парсит NMEA сообщение
+ */
+ public void parseNMEA(String nmeaSentence) {
+ if (nmeaSentence == null || nmeaSentence.trim().isEmpty()) {
+ return;
+ }
+
+ // Очищаем сообщение от лишних символов
+ String cleanedSentence = cleanNMEASentence(nmeaSentence);
+ if (cleanedSentence == null) {
+ Log.w(TAG, "NMEA сообщение не удалось очистить или слишком короткое: " + nmeaSentence);
+ return;
+ }
+ Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
+
+ // Отправляем NMEA сообщение на внешний ресурс
+ LogSender.logNMEA(cleanedSentence);
+
+ try {
+ // Разбираем сообщение по запятым
+ String[] fields = cleanedSentence.split(",");
+ if (fields.length < 2) {
+ Log.w(TAG, "NMEA сообщение слишком короткое: " + cleanedSentence);
+ return;
+ }
+
+ // Извлекаем приамбуду (первые 6 символов после $)
+ String preamble = fields[0];
+ if (preamble.length() < 6) {
+ Log.w(TAG, "Некорректная приамбула: " + preamble);
+ return;
+ }
+
+ // Определяем тип сообщения по последним трем символам приамбуды
+ String messageType = preamble.substring(preamble.length() - 3);
+
+ switch (messageType) {
+ case "GGA":
+ parseGGA(fields);
+ break;
+ case "RMC":
+ parseRMC(fields);
+ break;
+ case "VTG":
+ parseVTG(fields);
+ break;
+ case "GLL":
+ parseGLL(fields);
+ break;
+ case "GSV":
+ parseGSV(fields);
+ break;
+ case "GNS":
+ parseGNS(fields);
+ break;
+ case "GSA":
+ parseGSA(fields);
+ break;
+ case "ZDA":
+ parseZDA(fields);
+ break;
+ default:
+ // Проверяем AIS сообщения
+ if (cleanedSentence.startsWith("!AIVDM")) {
+ parseAIS(cleanedSentence);
+ } else {
+ Log.d(TAG, "Неподдерживаемый тип NMEA сообщения: " + messageType);
+ }
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка парсинга NMEA: " + e.getMessage(), e);
+ if (listener != null) {
+ listener.onParseError("Ошибка парсинга NMEA: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Безопасно получает поле по индексу
+ */
+ private String getField(String[] fields, int index) {
+ if (index < fields.length && !fields[index].trim().isEmpty()) {
+ return fields[index].trim();
+ }
+ return null;
+ }
+
+ /**
+ * Безопасно парсит double значение из поля
+ */
+ private double parseDoubleField(String[] fields, int index, double defaultValue) {
+ String field = getField(fields, index);
+ if (field != null) {
+ try {
+ return Double.parseDouble(field);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Не удалось распарсить double из поля " + index + ": '" + field + "'");
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Безопасно парсит int значение из поля
+ */
+ private int parseIntField(String[] fields, int index, int defaultValue) {
+ String field = getField(fields, index);
+ if (field != null) {
+ try {
+ return Integer.parseInt(field);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Не удалось распарсить int из поля " + index + ": '" + field + "'");
+ }
+ }
+ return defaultValue;
+ }
+
+ /**
+ * Очищает NMEA сообщение от лишних символов
+ */
+ private String cleanNMEASentence(String sentence) {
+ if (sentence == null || sentence.trim().isEmpty()) {
+ return null;
+ }
+
+ // Убираем пробелы в начале и конце
+ String cleaned = sentence.trim();
+
+ // Проверяем минимальную длину NMEA сообщения
+ if (cleaned.length() < 6) { // Минимум: $GPGGA*XX
+ Log.w(TAG, "Слишком короткое NMEA сообщение: '" + cleaned + "'");
+ return null;
+ }
+
+ // Исправляем двойной $ ($$GNGGA -> $GNGGA)
+ if (cleaned.startsWith("$$")) {
+ cleaned = cleaned.substring(1);
+ Log.d(TAG, "Исправлен двойной $: " + cleaned);
+ }
+
+ // Обрабатываем смешанные сообщения (например, VTG содержит GGA)
+ if (cleaned.contains("$G") && cleaned.indexOf("$G") > 0) {
+ // Находим первое полное NMEA сообщение
+ int firstDollar = cleaned.indexOf("$G");
+ if (firstDollar > 0) {
+ String firstMessage = cleaned.substring(firstDollar);
+ int asteriskIndex = firstMessage.indexOf('*');
+ if (asteriskIndex > 0) {
+ // Проверяем, что после * есть достаточно символов для контрольной суммы
+ if (asteriskIndex + 2 < firstMessage.length()) {
+ cleaned = firstMessage.substring(0, asteriskIndex + 3);
+ } else if (asteriskIndex + 1 < firstMessage.length()) {
+ cleaned = firstMessage.substring(0, asteriskIndex + 2);
+ } else {
+ cleaned = firstMessage.substring(0, asteriskIndex + 1);
+ }
+ Log.d(TAG, "Извлечено первое NMEA сообщение: " + cleaned);
+ }
+ }
+ }
+
+ // Убираем все символы после последнего *
+ int asteriskIndex = cleaned.lastIndexOf('*');
+ if (asteriskIndex >= 0) {
+ // Проверяем, что после * есть достаточно символов для контрольной суммы
+ 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); // включаем только *
+ }
+ }
+
+ // Убираем все непечатаемые символы
+ cleaned = cleaned.replaceAll("[^\\x20-\\x7E]", "");
+
+ Log.d(TAG, "Очищено NMEA: '" + cleaned + "' (длина: " + cleaned.length() + ")");
+
+ return cleaned;
+ }
+
+ /**
+ * Парсит GGA сообщение (Global Positioning System Fix Data)
+ * В гибридном режиме используем только количество спутников и высоту
+ * Формат: $GPGGA,time,lat,N/S,lon,E/W,quality,numSV,HDOP,alt,M,sep,M,diffAge,diffStation*checksum
+ */
+ private void parseGGA(String[] fields) {
+ Log.d(TAG, "Парсим GGA с " + fields.length + " полями");
+
+ // Поле 7: количество спутников
+ int satellites = parseIntField(fields, 7, 0);
+
+ // Поле 9: высота над эллипсоидом
+ double altitude = parseDoubleField(fields, 9, 0.0);
+
+ Log.d(TAG, String.format("GGA: sat=%d, alt=%.1f", satellites, altitude));
+
+ // В гибридном режиме не обновляем координаты
+ if (!hybridMode) {
+ // Поля 2,3: широта и направление
+ String latStr = getField(fields, 2);
+ String latDir = getField(fields, 3);
+ if (latStr != null && latDir != null) {
+ double latitude = parseCoordinate(latStr, latDir.equals("N"));
+ ownVessel.setLatitude(latitude);
+ }
+
+ // Поля 4,5: долгота и направление
+ String lonStr = getField(fields, 4);
+ String lonDir = getField(fields, 5);
+ if (lonStr != null && lonDir != null) {
+ double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
+ ownVessel.setLongitude(longitude);
+ }
+ }
+
+ ownVessel.setSatellites(satellites);
+ ownVessel.setAltitude(altitude);
+
+ // Синхронизируем с GPSLocationListener для получения активных спутников
+ if (gpsLocationListener != null) {
+ gpsLocationListener.setSatellitesInVessel(ownVessel);
+ }
+
+ if (listener != null) {
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+
+ /**
+ * Парсит RMC сообщение (Recommended Minimum Navigation Information)
+ * В гибридном режиме используем только курс и скорость
+ * Формат: $GPRMC,time,status,lat,N/S,lon,E/W,speed,course,date,magVar,E/W,mode*checksum
+ */
+ private void parseRMC(String[] fields) {
+ Log.d(TAG, "Парсим RMC с " + fields.length + " полями");
+
+ // Поле 2: статус валидности (A = валидный, V = невалидный)
+ String status = getField(fields, 2);
+ boolean isValid = status != null && status.startsWith("A");
+ Log.d(TAG, "RMC статус: " + status + " (валидный: " + isValid + ")");
+
+ // Поле 7: скорость в узлах
+ double speed = parseDoubleField(fields, 7, 0.0);
+
+ // Поле 8: курс в градусах
+ double course = parseDoubleField(fields, 8, 0.0);
+
+ Log.d(TAG, String.format("RMC: speed=%.1f, course=%.1f, valid=%s", speed, course, isValid));
+
+ // В гибридном режиме не обновляем координаты
+ if (!hybridMode && isValid) {
+ Log.d(TAG, "Режим НЕ гибридный - обрабатываем координаты из RMC");
+
+ // Поля 3,4: широта и направление
+ String latStr = getField(fields, 3);
+ String latDir = getField(fields, 4);
+ if (latStr != null && latDir != null) {
+ double latitude = parseCoordinate(latStr, latDir.equals("N"));
+ Log.d(TAG, "RMC широта: " + latStr + " " + latDir + " = " + latitude);
+ ownVessel.setLatitude(latitude);
+ }
+
+ // Поля 5,6: долгота и направление
+ String lonStr = getField(fields, 5);
+ String lonDir = getField(fields, 6);
+ if (lonStr != null && lonDir != null) {
+ double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
+ Log.d(TAG, "RMC долгота: " + lonStr + " " + lonDir + " = " + longitude);
+ ownVessel.setLongitude(longitude);
+ }
+ } else if (hybridMode) {
+ Log.d(TAG, "Гибридный режим - координаты из RMC игнорируются");
+ } else {
+ Log.d(TAG, "RMC данные невалидны (статус V) - координаты не обновляем");
+ }
+
+ // Обновляем скорость и курс только если данные валидны
+ if (isValid) {
+ ownVessel.setSpeed(speed);
+ ownVessel.setCourse(course);
+ }
+
+ Log.d(TAG, "RMC обновлено судно: lat=" + ownVessel.getLatitude() +
+ ", lon=" + ownVessel.getLongitude() +
+ ", speed=" + speed +
+ ", course=" + course);
+
+ if (listener != null) {
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+
+ /**
+ * Парсит VTG сообщение (Course Over Ground and Ground Speed)
+ * Формат: $GPVTG,course,T,course,M,speed,N,speed,K,mode*checksum
+ */
+ private void parseVTG(String[] fields) {
+ Log.d(TAG, "Парсим VTG с " + fields.length + " полями");
+
+ // Поле 1: курс в градусах (True)
+ double course = parseDoubleField(fields, 1, 0.0);
+
+ // Поле 5: скорость в узлах
+ double speed = parseDoubleField(fields, 5, 0.0);
+
+ Log.d(TAG, String.format("VTG: course=%.1f, speed=%.1f", course, speed));
+
+ ownVessel.setCourse(course);
+ ownVessel.setSpeed(speed);
+
+ if (listener != null) {
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+
+ /**
+ * Парсит GLL сообщение (Geographic Position - Latitude/Longitude)
+ * В гибридном режиме игнорируем
+ * Формат: $GPGLL,lat,N/S,lon,E/W,time,status,mode*checksum
+ */
+ private void parseGLL(String[] fields) {
+ if (hybridMode) {
+ Log.d(TAG, "GLL игнорируется в гибридном режиме");
+ return;
+ }
+
+ Log.d(TAG, "Парсим GLL с " + fields.length + " полями");
+
+ // Поля 1,2: широта и направление
+ String latStr = getField(fields, 1);
+ String latDir = getField(fields, 2);
+ if (latStr != null && latDir != null) {
+ double latitude = parseCoordinate(latStr, latDir.equals("N"));
+ ownVessel.setLatitude(latitude);
+ }
+
+ // Поля 3,4: долгота и направление
+ String lonStr = getField(fields, 3);
+ String lonDir = getField(fields, 4);
+ if (lonStr != null && lonDir != null) {
+ double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
+ ownVessel.setLongitude(longitude);
+ }
+
+ Log.d(TAG, String.format("GLL: lat=%.6f, lon=%.6f", ownVessel.getLatitude(), ownVessel.getLongitude()));
+
+ if (listener != null) {
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+
+ /**
+ * Парсит GSV сообщение (GPS Satellites in View)
+ * Формат: $GPGSV,totalMsgs,msgNum,totalSats,satId1,elev1,azim1,snr1,satId2,elev2,azim2,snr2,...*checksum
+ */
+ private void parseGSV(String[] fields) {
+ Log.d(TAG, "Парсим GSV с " + fields.length + " полями");
+
+ // Поля 1,2,3: общее количество сообщений, номер сообщения, общее количество спутников
+ int totalMessages = parseIntField(fields, 1, 1);
+ int messageNumber = parseIntField(fields, 2, 1);
+ int satellitesInView = parseIntField(fields, 3, 0);
+
+ // Определяем тип системы спутников по приамбуде
+ String systemType = "Unknown";
+ String preamble = fields[0];
+ if (preamble.startsWith("$GPGSV")) {
+ systemType = "GPS";
+ } else if (preamble.startsWith("$GLGSV")) {
+ systemType = "GLONASS";
+ } else if (preamble.startsWith("$GAGSV")) {
+ systemType = "Galileo";
+ } else if (preamble.startsWith("$GBGSV")) {
+ systemType = "BeiDou";
+ }
+
+ Log.d(TAG, String.format("GSV [%s]: %d/%d, спутников в поле зрения: %d",
+ systemType, messageNumber, totalMessages, satellitesInView));
+
+ // Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
+ for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
+ if (i + 3 < fields.length) {
+ String satId = getField(fields, i);
+ String elevation = getField(fields, i + 1);
+ String azimuth = getField(fields, i + 2);
+ String snr = getField(fields, i + 3);
+
+ if (satId != null) {
+ Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
+ satId, elevation, azimuth, snr));
+ }
+ }
+ }
+
+ // Обновляем количество спутников только для последнего сообщения в серии
+ if (messageNumber == totalMessages) {
+ // Обновляем количество спутников для соответствующей системы
+ switch (systemType) {
+ case "GPS":
+ gpsSatellites = satellitesInView;
+ break;
+ case "GLONASS":
+ glonassSatellites = satellitesInView;
+ break;
+ case "Galileo":
+ galileoSatellites = satellitesInView;
+ break;
+ case "BeiDou":
+ // Пока не добавляем отдельный счетчик для BeiDou, считаем как GPS
+ gpsSatellites = Math.max(gpsSatellites, satellitesInView);
+ break;
+ }
+
+ // Обновляем общее количество спутников
+ int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites;
+ ownVessel.setSatellites(totalSatellites);
+
+ // Синхронизируем с GPSLocationListener для получения активных спутников
+ if (gpsLocationListener != null) {
+ gpsLocationListener.setSatellitesInVessel(ownVessel);
+ }
+
+ Log.d(TAG, String.format("GSV [%s] завершен: %d спутников. Общий счет: GPS=%d, GLONASS=%d, Galileo=%d, Всего=%d",
+ systemType, satellitesInView, gpsSatellites, glonassSatellites, galileoSatellites, totalSatellites));
+
+ if (listener != null) {
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+ }
+
+ /**
+ * Парсит GNS сообщение (GNSS Fix Data)
+ * В гибридном режиме используем только количество спутников и высоту
+ * Формат: $GNGNS,time,lat,N/S,lon,E/W,mode,numSV,HDOP,alt,sep,diffAge,diffStation,navStatus*checksum
+ */
+ private void parseGNS(String[] fields) {
+ Log.d(TAG, "Парсим GNS с " + fields.length + " полями");
+
+ // Поле 7: количество спутников
+ int satellites = parseIntField(fields, 7, 0);
+
+ // Поле 9: высота над эллипсоидом
+ double altitude = parseDoubleField(fields, 9, 0.0);
+
+ Log.d(TAG, String.format("GNS: sat=%d, alt=%.1f", satellites, altitude));
+
+ // В гибридном режиме не обновляем координаты
+ if (!hybridMode) {
+ // Поля 2,3: широта и направление
+ String latStr = getField(fields, 2);
+ String latDir = getField(fields, 3);
+ if (latStr != null && latDir != null) {
+ double latitude = parseCoordinate(latStr, latDir.equals("N"));
+ ownVessel.setLatitude(latitude);
+ }
+
+ // Поля 4,5: долгота и направление
+ String lonStr = getField(fields, 4);
+ String lonDir = getField(fields, 5);
+ if (lonStr != null && lonDir != null) {
+ double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
+ ownVessel.setLongitude(longitude);
+ }
+ }
+
+ ownVessel.setSatellites(satellites);
+ ownVessel.setAltitude(altitude);
+
+ // Синхронизируем с GPSLocationListener для получения активных спутников
+ if (gpsLocationListener != null) {
+ gpsLocationListener.setSatellitesInVessel(ownVessel);
+ }
+
+ if (listener != null) {
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+
+ /**
+ * Парсит ZDA сообщение (Date and Time)
+ * Формат: $GPZDA,time,day,month,year,timezoneHours,timezoneMinutes*checksum
+ */
+ private void parseZDA(String[] fields) {
+ Log.d(TAG, "Парсим ZDA с " + fields.length + " полями");
+
+ try {
+ // Поле 1: время (HHMMSS.SS)
+ String timeStr = getField(fields, 1);
+
+ // Поля 2,3,4: день, месяц, год
+ int day = parseIntField(fields, 2, 0);
+ int month = parseIntField(fields, 3, 0);
+ int year = parseIntField(fields, 4, 0);
+
+ // Поля 5,6: часовой пояс (часы и минуты)
+ int timezoneHours = parseIntField(fields, 5, 0);
+ int timezoneMinutes = parseIntField(fields, 6, 0);
+
+ 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 (Exception e) {
+ Log.w(TAG, "Ошибка парсинга ZDA: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Парсит GSA сообщение (GPS DOP and Active Satellites)
+ * КЛЮЧЕВОЕ сообщение для получения DOP и активных спутников
+ * Формат: $GPGSA,mode,fixType,sat1,sat2,...,sat12,PDOP,HDOP,VDOP*checksum
+ */
+ private void parseGSA(String[] fields) {
+ Log.d(TAG, "Парсим GSA с " + fields.length + " полями");
+
+ // Подсчитываем активные спутники (поля 3-14 содержат ID спутников)
+ int activeSatellites = 0;
+ for (int i = 3; i <= 14 && i < fields.length; i++) {
+ String satId = getField(fields, i);
+ if (satId != null && !satId.equals("0")) {
+ activeSatellites++;
+ Log.d(TAG, "Активный спутник: " + satId);
+ }
+ }
+
+ // Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
+ double pdop = 0.0;
+ double hdop = 0.0;
+ double vdop = 0.0;
+
+ // DOP значения обычно в последних полях перед контрольной суммой
+ if (fields.length >= 17) {
+ // Полное GSA сообщение
+ pdop = parseDoubleField(fields, 15, 0.0); // PDOP
+ hdop = parseDoubleField(fields, 16, 0.0); // HDOP
+ vdop = parseDoubleField(fields, 17, 0.0); // VDOP
+ } else if (fields.length >= 6) {
+ // Обрезанное GSA сообщение - DOP в последних полях
+ int dopStartIndex = fields.length - 4; // -4 чтобы исключить контрольную сумму
+ if (dopStartIndex >= 3) {
+ pdop = parseDoubleField(fields, dopStartIndex, 0.0);
+ hdop = parseDoubleField(fields, dopStartIndex + 1, 0.0);
+ vdop = parseDoubleField(fields, dopStartIndex + 2, 0.0);
+ }
+ }
+
+ Log.d(TAG, String.format("GSA: активных спутников=%d, PDOP=%.2f, HDOP=%.2f, VDOP=%.2f",
+ activeSatellites, pdop, hdop, vdop));
+
+ // Обновляем информацию о спутниках
+ ownVessel.setActiveSatellites(activeSatellites);
+ ownVessel.setPdop(pdop);
+ ownVessel.setHdop(hdop);
+ ownVessel.setVdop(vdop);
+
+ // Отправляем DOP значения в GPS Location Listener
+ if (gpsLocationListener != null) {
+ gpsLocationListener.setDOPValues(pdop, hdop, vdop);
+ // Синхронизируем с GPSLocationListener для получения активных спутников
+ gpsLocationListener.setSatellitesInVessel(ownVessel);
+ }
+
+ // Уведомляем слушателя о DOP
+ if (listener != null) {
+ listener.onDOPUpdated(pdop, hdop, vdop);
+ listener.onVesselUpdated(ownVessel);
+ }
+ }
+
+ /**
+ * Парсит AIS сообщение (Automatic Identification System)
+ * Формат: !AIVDM,totalFragments,fragmentNumber,sequenceId,channel,payload,fillBits*checksum
+ */
+ private void parseAIS(String ais) {
+ Log.d(TAG, "Парсим AIS: " + ais);
+
+ // Разбираем AIS сообщение по запятым
+ String[] fields = ais.split(",");
+ Log.d(TAG, "AIS поля (" + fields.length + "): " + java.util.Arrays.toString(fields));
+ if (fields.length < 7) {
+ Log.w(TAG, "AIS сообщение слишком короткое: " + ais);
+ return;
+ }
+
+ try {
+ // Поля 1,2: общее количество фрагментов, номер фрагмента
+ int totalFragments = parseIntField(fields, 1, 1);
+ int fragmentNumber = parseIntField(fields, 2, 1);
+
+ // Поле 3: ID последовательности
+ String sequenceId = getField(fields, 3);
+
+ // Поле 4: канал (A или B)
+ String channel = getField(fields, 4);
+
+ // Поле 5: payload (данные)
+ String payload = getField(fields, 5);
+
+ // Поле 6: количество бит заполнения (может содержать *checksum)
+ String fillBitsField = getField(fields, 6);
+ int fillBits = 0;
+ if (fillBitsField != null) {
+ // Если поле содержит *, берем только часть до *
+ if (fillBitsField.contains("*")) {
+ fillBitsField = fillBitsField.split("\\*")[0];
+ }
+ try {
+ fillBits = Integer.parseInt(fillBitsField);
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Не удалось распарсить fillBits из поля 6: '" + fillBitsField + "'");
+ }
+ }
+
+ // Контрольная сумма находится в последнем поле после *
+ String lastField = fields[fields.length - 1];
+ String checksum = null;
+ if (lastField != null && lastField.contains("*")) {
+ String[] parts = lastField.split("\\*");
+ if (parts.length > 1) {
+ checksum = parts[1];
+ }
+ }
+
+ Log.d(TAG, String.format("AIS: %d/%d, seq='%s', ch='%s', payload='%s', fillBits=%d, checksum='%s'",
+ fragmentNumber, totalFragments, sequenceId, channel, payload, fillBits, checksum));
+
+ // Проверяем контрольную сумму
+ if (!validateChecksum(ais)) {
+ Log.w(TAG, "AIS сообщение с неверной контрольной суммой: " + ais);
+ return;
+ }
+
+ // Проверяем, что payload не пустой
+ if (payload != null && !payload.trim().isEmpty()) {
+ if (totalFragments == 1) {
+ // Одноканальное сообщение - декодируем сразу
+ decodeAISPayload(payload, channel != null && channel.equals("A") ? 0 : 1);
+ } else {
+ // Многочастное сообщение - собираем фрагменты
+ // Используем номер фрагмента как sequenceId если поле пустое
+ String actualSequenceId = (sequenceId != null && !sequenceId.trim().isEmpty()) ?
+ sequenceId : String.valueOf(fragmentNumber);
+ collectAISFragments(actualSequenceId, fragmentNumber, totalFragments, payload, channel != null && channel.equals("A") ? 0 : 1);
+ }
+ } else {
+ Log.w(TAG, "AIS payload пустой, пропускаем сообщение");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка парсинга AIS сообщения: " + e.getMessage() + " для сообщения: " + ais);
+ if (listener != null) {
+ listener.onParseError("Ошибка парсинга AIS: " + e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Декодирует AIS payload
+ */
+ private void decodeAISPayload(String payload, int channel) {
+ try {
+ // Определяем тип AIS сообщения по первым 6 битам
+ String messageTypeBits = decodeAISField(payload, 0, 6);
+ int messageType = Integer.parseInt(messageTypeBits, 2);
+
+ Log.d(TAG, "Декодируем AIS тип " + messageType + " на канале " + channel + " (биты: " + messageTypeBits + ")");
+
+ switch (messageType) {
+ case 1:
+ case 2:
+ case 3:
+ // Position Report
+ Log.d(TAG, "Обрабатываем Position Report (тип " + messageType + ")");
+ decodePositionReport(payload, messageType);
+ break;
+ case 5:
+ // Static Data
+ Log.d(TAG, "Обрабатываем Static Data (тип " + messageType + ")");
+ decodeStaticData(payload);
+ break;
+ case 4: // Base Station Report
+ Log.d(TAG, "Обрабатываем Base Station Report (тип " + messageType + ")");
+ decodeBaseStationReport(payload);
+ break;
+ case 14: // Safety Related Broadcast Message
+ Log.d(TAG, "Обрабатываем Safety Broadcast (тип " + messageType + ")");
+ decodeSafetyBroadcast(payload);
+ break;
+ case 18: // Standard Class B Equipment Position Report
+ Log.d(TAG, "Обрабатываем Class B Position Report (тип " + messageType + ")");
+ decodeClassBPositionReport(payload);
+ break;
+ case 19: // Extended Class B Equipment Position Report
+ Log.d(TAG, "Обрабатываем Extended Class B Position Report (тип " + messageType + ")");
+ decodeExtendedClassBPositionReport(payload);
+ break;
+ case 21: // Aid-to-Navigation Report
+ Log.d(TAG, "Обрабатываем Aid-to-Navigation Report (тип " + messageType + ")");
+ decodeAidToNavigationReport(payload);
+ break;
+ case 24: // Static Data Report
+ Log.d(TAG, "Обрабатываем Static Data Report (тип " + messageType + ")");
+ decodeStaticDataReport(payload);
+ break;
+ default:
+ Log.d(TAG, "Неподдерживаемый тип AIS сообщения: " + messageType);
+ break;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования AIS payload: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Собирает фрагменты многочастного AIS сообщения
+ */
+ private void collectAISFragments(String sequenceId, int fragmentNumber, int totalFragments,
+ String payload, int channel) {
+ String key = sequenceId + "_" + channel;
+
+ Log.d(TAG, String.format("Собираем AIS фраг мент: %d/%d для %s",
+ fragmentNumber, totalFragments, key));
+
+ // Очищаем старые фрагменты
+ cleanupOldFragments();
+
+ // Получаем или создаем карту фрагментов для этой последовательности
+ java.util.Map fragments = aisFragments.get(key);
+ if (fragments == null) {
+ fragments = new java.util.HashMap<>();
+ aisFragments.put(key, fragments);
+ aisFragmentTimestamps.put(key, System.currentTimeMillis());
+ Log.d(TAG, "Создан новый набор фрагментов для: " + key);
+ }
+
+ // Добавляем фрагмент
+ fragments.put(fragmentNumber, payload);
+ Log.d(TAG, String.format("Добавлен фрагмент %d/%d для %s",
+ fragmentNumber, totalFragments, key));
+
+ // Проверяем, все ли фрагменты получены
+ if (fragments.size() == totalFragments) {
+ Log.d(TAG, "Все фрагменты получены для " + key + ", собираем сообщение");
+
+ // Собираем полное сообщение
+ StringBuilder fullPayload = new StringBuilder();
+ for (int i = 1; i <= totalFragments; i++) {
+ String fragment = fragments.get(i);
+ if (fragment != null) {
+ fullPayload.append(fragment);
+ } else {
+ Log.w(TAG, "Отсутствует фрагмент " + i + " для " + key);
+ return;
+ }
+ }
+
+ String completePayload = fullPayload.toString();
+ Log.d(TAG, "Собрано полное AIS сообщение длиной " + completePayload.length() + " символов");
+
+ // Декодируем полное сообщение
+ decodeAISPayload(completePayload, channel);
+
+ // Удаляем собранные фрагменты
+ aisFragments.remove(key);
+ aisFragmentTimestamps.remove(key);
+ Log.d(TAG, "Фрагменты удалены для " + key);
+ } else {
+ Log.d(TAG, String.format("Ожидаем еще %d фрагментов для %s",
+ totalFragments - fragments.size(), key));
+ }
+ }
+
+ /**
+ * Очищает старые AIS фрагменты
+ */
+ private void cleanupOldFragments() {
+ long currentTime = System.currentTimeMillis();
+ java.util.Iterator> iterator = aisFragmentTimestamps.entrySet().iterator();
+
+ while (iterator.hasNext()) {
+ java.util.Map.Entry entry = iterator.next();
+ if (currentTime - entry.getValue() > AIS_FRAGMENT_TIMEOUT) {
+ String key = entry.getKey();
+ aisFragments.remove(key);
+ iterator.remove();
+ Log.d(TAG, "Удален устаревший AIS фрагмент: " + key);
+ }
+ }
+ }
+
+ /**
+ * Декодирует AIS поле из битовой строки
+ */
+ private String decodeAISField(String payload, int startBit, int length) {
+ StringBuilder result = new StringBuilder();
+
+ // Преобразуем каждый символ payload в 6-битное значение
+ for (int i = 0; i < payload.length(); i++) {
+ int ascii = payload.charAt(i);
+ int value;
+
+ if (ascii >= 48 && ascii <= 87) {
+ value = ascii - 48; // '0'..'W'
+ } else if (ascii >= 88 && ascii <= 119) {
+ value = ascii - 56; // 'X'..'w'
+ } else {
+ throw new IllegalArgumentException("Недопустимый символ AIS payload: " + (char)ascii);
+ }
+
+ // Дополняем до 6 бит слева нулями и добавляем в общую строку
+ String binary = String.format("%6s", Integer.toBinaryString(value)).replace(' ', '0');
+ result.append(binary);
+ }
+
+ String fullBinary = result.toString();
+
+ // Вырезаем нужный диапазон битов
+ if (startBit + length <= fullBinary.length()) {
+ String fieldResult = fullBinary.substring(startBit, startBit + length);
+ // Дополнительное логирование для первых 6 бит (тип сообщения)
+ if (startBit == 0 && length == 6) {
+ Log.d(TAG, "AIS Message Type bits: " + fieldResult + " (payload: " + payload + ")");
+ }
+ return fieldResult;
+ } else {
+ Log.w(TAG,
+ "AIS поле выходит за границы: startBit=" + startBit +
+ ", length=" + length +
+ ", payloadLength=" + payload.length() +
+ ", binaryLength=" + fullBinary.length()
+ );
+ // Если поле выходит за границы, возвращаем то что есть, дополняя нулями
+ if (startBit >= fullBinary.length()) {
+ // Если startBit уже за границами, возвращаем строку из нулей
+ return "0".repeat(length);
+ } else {
+ // Возвращаем доступную часть, дополняя нулями до нужной длины
+ String available = fullBinary.substring(startBit);
+ if (available.length() < length) {
+ available += "0".repeat(length - available.length());
+ }
+ return available;
+ }
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 1, 2, 3 (Position Report)
+ */
+ private void decodePositionReport(String payload, int messageType) {
+ try {
+ Log.d(TAG, "Декодируем Position Report тип " + messageType + ", payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Navigation Status (4 бита) - бит 38
+ String statusBits = decodeAISField(payload, 38, 4);
+ int status = Integer.parseInt(statusBits, 2);
+ Log.d(TAG, "Status bits: " + statusBits + " = " + status);
+
+ // Rate of Turn (8 бит) - бит 42
+ String rotBits = decodeAISField(payload, 42, 8);
+ double rateOfTurn = parseAISRateOfTurn(rotBits);
+ Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rateOfTurn + " °/мин");
+
+ // Speed Over Ground (10 бит) - бит 50
+ String speedBits = decodeAISField(payload, 50, 10);
+ double speed = Integer.parseInt(speedBits, 2) / 10.0;
+ Log.d(TAG, "Speed bits: " + speedBits + " = " + speed);
+
+ // Position Accuracy (1 бит) - бит 60
+ String accuracyBits = decodeAISField(payload, 60, 1);
+ int accuracy = Integer.parseInt(accuracyBits, 2);
+ Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
+
+ // Longitude (28 бит) - бит 61
+ String lonBits = decodeAISField(payload, 61, 28);
+ double longitude = parseAISCoordinate(lonBits, 28);
+ Log.d(TAG, "Longitude bits: " + lonBits + " (длина: " + lonBits.length() + ") = " + longitude);
+
+ // Latitude (27 бит) - бит 89
+ String latBits = decodeAISField(payload, 89, 27);
+ double latitude = parseAISCoordinate(latBits, 27);
+ Log.d(TAG, "Latitude bits: " + latBits + " (длина: " + latBits.length() + ") = " + latitude);
+
+ // Course Over Ground (12 бит) - бит 116
+ String courseBits = decodeAISField(payload, 116, 12);
+ double course = Integer.parseInt(courseBits, 2) / 10.0;
+ Log.d(TAG, "Course bits: " + courseBits + " = " + course);
+
+ // True Heading (9 бит) - бит 128
+ String headingBits = decodeAISField(payload, 128, 9);
+ double heading = Integer.parseInt(headingBits, 2);
+ Log.d(TAG, "Heading bits: " + headingBits + " = " + heading);
+
+ // Time Stamp (6 бит) - бит 137
+ String timestampBits = decodeAISField(payload, 137, 6);
+ int timestamp = Integer.parseInt(timestampBits, 2);
+ Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp);
+
+ // Проверяем, что координаты в разумных пределах
+ if (latitude < -90 || latitude > 90) {
+ Log.w(TAG, "Широта вне допустимых пределов: " + latitude);
+ }
+ if (longitude < -180 || longitude > 180) {
+ Log.w(TAG, "Долгота вне допустимых пределов: " + longitude);
+ }
+
+ Log.d(TAG, String.format("AIS Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%d, heading=%.1f, ROT=%.1f",
+ mmsi, latitude, longitude, course, speed, status, heading, rateOfTurn));
+
+ // Создаем или обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.updatePosition(latitude, longitude, course, speed, rateOfTurn);
+ vessel.setHeading(heading);
+ vessel.setNavigationalStatus(getNavigationStatus(status));
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+
+ // Отправляем информацию о корабле на внешний ресурс
+ String vesselInfo = String.format("lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%s, ROT=%.1f",
+ latitude, longitude, course, speed, getNavigationStatus(status), rateOfTurn);
+ LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Position Report: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 5 (Static Data)
+ */
+ private void decodeStaticData(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Static Data, payload: " + payload + " (длина: " + payload.length() + ")");
+ Log.d(TAG, "Общая длина в битах: " + (payload.length() * 6));
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // AIS Version (2 бита) - бит 38
+ String aisVersionBits = decodeAISField(payload, 38, 2);
+ int aisVersion = Integer.parseInt(aisVersionBits, 2);
+ Log.d(TAG, "AIS Version bits: " + aisVersionBits + " = " + aisVersion);
+
+ // IMO Number (30 бит) - бит 40
+ String imoBits = decodeAISField(payload, 40, 30);
+ int imo = Integer.parseInt(imoBits, 2);
+ Log.d(TAG, "IMO bits: " + imoBits + " = " + imo);
+
+ // Call Sign (42 бита) - бит 70
+ String callSignBits = decodeAISField(payload, 70, 42);
+ String callSign = decodeAISString(callSignBits);
+ Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'");
+
+ // Vessel Name (120 бит) - бит 112
+ String nameBits = decodeAISField(payload, 112, 120);
+ String vesselName = decodeAISString(nameBits);
+ Log.d(TAG, "Name bits: " + nameBits + " = '" + vesselName + "'");
+
+ // Ship Type (8 бит) - бит 232
+ String typeBits = decodeAISField(payload, 232, 8);
+ int vesselTypeCode = Integer.parseInt(typeBits, 2);
+ Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode);
+
+ // Dimension Reference (9, 9, 6, 6 бит) - бит 240
+ String dimRefABits = decodeAISField(payload, 240, 9);
+ String dimRefBBits = decodeAISField(payload, 249, 9);
+ String dimRefCBits = decodeAISField(payload, 258, 6);
+ String dimRefDBits = decodeAISField(payload, 264, 6);
+
+ int dimRefA = Integer.parseInt(dimRefABits, 2);
+ int dimRefB = Integer.parseInt(dimRefBBits, 2);
+ int dimRefC = Integer.parseInt(dimRefCBits, 2);
+ int dimRefD = Integer.parseInt(dimRefDBits, 2);
+
+ Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD);
+
+ // Для сообщения типа 5 используем Dimension Reference поля (9, 9, 6, 6 бит)
+ // Размеры судна рассчитываются как:
+ // Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
+ // Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
+ double length = dimRefA + dimRefB;
+ double width = dimRefC + dimRefD;
+
+ // Draft (8 бит) - осадка - бит 294
+ String draftBits = decodeAISField(payload, 294, 8);
+ double draft = Integer.parseInt(draftBits, 2) / 10.0;
+
+ Log.d(TAG, "Static Data - используем Dimension Reference поля (9, 9, 6, 6 бит):");
+ Log.d(TAG, " Dim.A (нос-антенна): " + dimRefABits + " = " + dimRefA + " м");
+ Log.d(TAG, " Dim.B (антенна-корма): " + dimRefBBits + " = " + dimRefB + " м");
+ Log.d(TAG, " Dim.C (левый борт-антенна): " + dimRefCBits + " = " + dimRefC + " м");
+ Log.d(TAG, " Dim.D (антенна-правый борт): " + dimRefDBits + " = " + dimRefD + " м");
+ Log.d(TAG, " Total Length (A+B): " + length + " м");
+ Log.d(TAG, " Total Width (C+D): " + width + " м");
+ Log.d(TAG, " Draft: " + draftBits + " = " + draft + " м");
+
+ // ETA (20 бит) - бит 274
+ String etaBits = decodeAISField(payload, 274, 20);
+ int eta = Integer.parseInt(etaBits, 2);
+ Log.d(TAG, "ETA bits: " + etaBits + " = " + eta);
+
+ // Парсим ETA согласно стандарту: MMDDHHMM UTC
+ // Bits 19-16: month; 1-12; 0 = not available = default
+ // Bits 15-11: day; 1-31; 0 = not available = default
+ // Bits 10-6: hour; 0-23; 24 = not available = default
+ // Bits 5-0: minute; 0-59; 60 = not available = default
+ java.time.LocalDateTime etaDateTime = parseETA(eta);
+ Log.d(TAG, "ETA parsed: " + etaDateTime);
+
+ // Вычисляем доступную длину для оставшихся полей
+ int totalBits = payload.length() * 6;
+ int remainingBits = totalBits - 294; // Остается после ETA
+ Log.d(TAG, "Remaining bits after ETA: " + remainingBits + " (total: " + totalBits + ")");
+
+ String destination = "";
+ double maxDraught = 0.0;
+ String epfdDescription = "Unknown";
+ boolean dteReady = false;
+
+ // Destination (120 бит) - бит 302
+ if (totalBits >= 302 + 120) {
+ String destBits = decodeAISField(payload, 302, 120);
+ destination = decodeAISString(destBits);
+ Log.d(TAG, "Destination bits: " + destBits + " = '" + destination + "'");
+ } else if (remainingBits > 0) {
+ // Если сообщение короткое, читаем доступные биты
+ int destStartBit = 302;
+ int destLength = Math.min(remainingBits, 120);
+ String destBits = decodeAISField(payload, destStartBit, destLength);
+ destination = decodeAISString(destBits);
+ Log.d(TAG, "Destination bits (short): " + destBits + " = '" + destination + "' (length: " + destLength + ")");
+ }
+
+ Log.d(TAG, String.format("AIS Static: MMSI=%d, IMO=%d, name='%s', callSign='%s', type=%d, L=%.1f, W=%.1f, D=%.1f, maxD=%.1f, ETA=%s, EPFD=%s, DTE=%s, dest='%s'",
+ mmsi, imo, vesselName, callSign, vesselTypeCode, length, width, draft, maxDraught, etaDateTime, epfdDescription, dteReady, destination));
+
+ // Обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.setVesselName(vesselName);
+ vessel.setCallSign(callSign);
+ vessel.setImo(imo);
+ vessel.setVesselType(getVesselType(vesselTypeCode));
+ vessel.setLength(length);
+ vessel.setWidth(width);
+ vessel.setDraft(draft);
+ vessel.setDestination(destination);
+ vessel.setEta(etaDateTime); // Добавляем ETA в модель
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+
+ // Отправляем информацию о корабле на внешний ресурс
+ String vesselInfo = String.format("name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f, dest='%s'",
+ vesselName, callSign, getVesselType(vesselTypeCode), length, width, draft, destination);
+ LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Static Data: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Парсит ETA (Estimated Time of Arrival) из 20-битного значения
+ * Формат: MMDDHHMM UTC
+ * Bits 19-16: month; 1-12; 0 = not available = default
+ * Bits 15-11: day; 1-31; 0 = not available = default
+ * Bits 10-6: hour; 0-23; 24 = not available = default
+ * Bits 5-0: minute; 0-59; 60 = not available = default
+ */
+ private java.time.LocalDateTime parseETA(int eta) {
+ if (eta == 0) {
+ return null; // Not available
+ }
+
+ Log.d(TAG, "ETA raw value: " + eta + " (binary: " + Integer.toBinaryString(eta) + ")");
+
+ // Извлекаем компоненты из 20-битного значения
+ // Правильный порядок битов: MMMM DDDDD HHHHH MMMMMM
+ int month = (eta >> 16) & 0x0F; // Bits 19-16 (4 бита)
+ int day = (eta >> 11) & 0x1F; // Bits 15-11 (5 бит)
+ int hour = (eta >> 6) & 0x1F; // Bits 10-6 (5 бит)
+ int minute = eta & 0x3F; // Bits 5-0 (6 бит)
+
+ Log.d(TAG, String.format("ETA components: month=%d, day=%d, hour=%d, minute=%d",
+ month, day, hour, minute));
+
+ // Проверяем на значения по умолчанию
+ if (month == 0 || month > 12) return null; // Not available
+ if (day == 0 || day > 31) return null; // Not available
+ if (hour == 24 || hour > 23) return null; // Not available
+ if (minute == 60 || minute > 59) return null; // Not available
+
+ try {
+ // Создаем LocalDateTime для текущего года
+ int currentYear = java.time.LocalDate.now().getYear();
+ java.time.LocalDateTime etaDateTime = java.time.LocalDateTime.of(
+ currentYear, month, day, hour, minute);
+
+ Log.d(TAG, "ETA parsed as LocalDateTime: " + etaDateTime);
+ return etaDateTime;
+ } catch (Exception e) {
+ Log.w(TAG, "Ошибка создания LocalDateTime для ETA: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Парсит AIS Rate of Turn (скорость поворота)
+ * Согласно стандарту ITU-R M.1371-5, таблица 47
+ */
+ private double parseAISRateOfTurn(String bits) {
+ int value = Integer.parseInt(bits, 2);
+
+ // Специальные значения согласно стандарту
+ if (value == 0) {
+ return 0.0; // Не поворачивается
+ } else if (value == 127) {
+ return 0.0; // Неопределенное значение
+ } else if (value >= 1 && value <= 126) {
+ // Поворот вправо: ROT = value / 4.733
+ return value / 4.733;
+ } else if (value >= 128 && value <= 255) {
+ // Поворот влево: ROT = -(value - 128) / 4.733
+ return -(value - 128) / 4.733;
+ } else {
+ return 0.0; // Неопределенное значение
+ }
+ }
+
+ /**
+ * Парсит AIS координаты
+ */
+ } else if (value == 2) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 3) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 4) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 5) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 6) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 7) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 8) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 9) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 10) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 11) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 12) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 13) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 14) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 15) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 16) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 17) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 18) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 19) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 20) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 21) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 22) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 23) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 24) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 25) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 26) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 27) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 28) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 29) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 30) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 31) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 32) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 33) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 34) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 35) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 36) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 37) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 38) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 39) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 40) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 41) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 42) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 43) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 44) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 45) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 46) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 47) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 48) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 49) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 50) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 51) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 52) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 53) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 54) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 55) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 56) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 57) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 58) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 59) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 60) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 61) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 62) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 63) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 64) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 65) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 66) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 67) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 68) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 69) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 70) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 71) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 72) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 73) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 74) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 75) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 76) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 77) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 78) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 79) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 80) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 81) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 82) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 83) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 84) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 85) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 86) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 87) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 88) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 89) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 90) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 91) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 92) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 93) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 94) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 95) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 96) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 97) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 98) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 99) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 100) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 101) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 102) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 103) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 104) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 105) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 106) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 107) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 108) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 109) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 110) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 111) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 112) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 113) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 114) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 115) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 116) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 117) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 118) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 119) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 120) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 121) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 122) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 123) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 124) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 125) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 126) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 127) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 128) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 129) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 130) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 131) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 132) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 133) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 134) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 135) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 136) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 137) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 138) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 139) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 140) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 141) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 142) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 143) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 144) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 145) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 146) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 147) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 148) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 149) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 150) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 151) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 152) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 153) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 154) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 155) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 156) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 157) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 158) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 159) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 160) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 161) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 162) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 163) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 164) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 165) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 166) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 167) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 168) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 169) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 170) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 171) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 172) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 173) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 174) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 175) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 176) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 177) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 178) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 179) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 180) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 181) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 182) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 183) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 184) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 185) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 186) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 187) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 188) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 189) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 190) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 191) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 192) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 193) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 194) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 195) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 196) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 197) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 198) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 199) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 200) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 201) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 202) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 203) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 204) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 205) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 206) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 207) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 208) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 209) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 210) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 211) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 212) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 213) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 214) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 215) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 216) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 217) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 218) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 219) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 220) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 221) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 222) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 223) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 224) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 225) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 226) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 227) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 228) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 229) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 230) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 231) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 232) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 233) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 234) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 235) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 236) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 237) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 238) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 239) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 240) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 241) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 242) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 243) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 244) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 245) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 246) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 247) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 248) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 249) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 250) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 251) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 252) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 253) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else if (value == 254) {
+ return 0.0; // Поворачивается влево со скоростью более 5°/мин
+ } else if (value == 255) {
+ return 0.0; // Поворачивается вправо со скоростью более 5°/мин
+ } else {
+ // Для значений 1-126: поворот вправо
+ // Для значений 128-255: поворот влево
+ // Формула: ROT = (value - 128) / 4.733
+ if (value >= 1 && value <= 126) {
+ // Поворот вправо: ROT = value / 4.733
+ return value / 4.733;
+ } else if (value >= 128 && value <= 255) {
+ // Поворот влево: ROT = -(value - 128) / 4.733
+ return -(value - 128) / 4.733;
+ } else {
+ return 0.0; // Неопределенное значение
+ }
+ }
+ }
+
+ /**
+ * Парсит AIS координаты
+ */
+ private double parseAISCoordinate(String bits, int bitLength) {
+ // Проверяем знаковый бит
+ boolean isNegative = bits.charAt(0) == '1';
+
+ // Преобразуем в беззнаковое число
+ long value = Long.parseLong(bits, 2);
+
+ if (bitLength == 27) {
+ // Широта: 27 бит, диапазон -90 до +90
+ if (isNegative) {
+ // Для отрицательных чисел применяем дополнение до двух
+ value = value - (1L << 27);
+ }
+ return value / 600000.0;
+ } else {
+ // Долгота: 28 бит, диапазон -180 до +180
+ if (isNegative) {
+ // Для отрицательных чисел применяем дополнение до двух
+ value = value - (1L << 28);
+ }
+ return value / 600000.0;
+ }
+ }
+
+ /**
+ * Декодирует AIS строку согласно стандарту ITU-R M.1371-5, таблица 44
+ * Простой switch case для всех 64 возможных значений 6-битной кодировки
+ */
+ private String decodeAISString(String bits) {
+ StringBuilder result = new StringBuilder();
+
+ Log.d(TAG, "Декодируем AIS строку, биты: " + bits + " (длина: " + bits.length() + ")");
+
+ for (int i = 0; i + 6 <= bits.length(); i += 6) {
+ String charBits = bits.substring(i, i + 6);
+ int value = Integer.parseInt(charBits, 2);
+
+ char decodedChar;
+ // Простой switch case для всех 64 возможных значений
+ switch (value) {
+ case 0: decodedChar = ' '; break;
+ case 1: decodedChar = 'A'; break;
+ case 2: decodedChar = 'B'; break;
+ case 3: decodedChar = 'C'; break;
+ case 4: decodedChar = 'D'; break;
+ case 5: decodedChar = 'E'; break;
+ case 6: decodedChar = 'F'; break;
+ case 7: decodedChar = 'G'; break;
+ case 8: decodedChar = 'H'; break;
+ case 9: decodedChar = 'I'; break;
+ case 10: decodedChar = 'J'; break;
+ case 11: decodedChar = 'K'; break;
+ case 12: decodedChar = 'L'; break;
+ case 13: decodedChar = 'M'; break;
+ case 14: decodedChar = 'N'; break;
+ case 15: decodedChar = 'O'; break;
+ case 16: decodedChar = 'P'; break;
+ case 17: decodedChar = 'Q'; break;
+ case 18: decodedChar = 'R'; break;
+ case 19: decodedChar = 'S'; break;
+ case 20: decodedChar = 'T'; break;
+ case 21: decodedChar = 'U'; break;
+ case 22: decodedChar = 'V'; break;
+ case 23: decodedChar = 'W'; break;
+ case 24: decodedChar = 'X'; break;
+ case 25: decodedChar = 'Y'; break;
+ case 26: decodedChar = 'Z'; break;
+ case 27: decodedChar = '0'; break;
+ case 28: decodedChar = '1'; break;
+ case 29: decodedChar = '2'; break;
+ case 30: decodedChar = '3'; break;
+ case 31: decodedChar = '4'; break;
+ case 32: decodedChar = ' '; break; // пробел
+ case 33: decodedChar = '5'; break;
+ case 34: decodedChar = '6'; break;
+ case 35: decodedChar = '7'; break;
+ case 36: decodedChar = '8'; break;
+ case 37: decodedChar = '9'; break;
+ case 38: decodedChar = ' '; break; // пробел
+ case 39: decodedChar = ' '; break; // пробел
+ case 40: decodedChar = ' '; break; // пробел
+ case 41: decodedChar = ' '; break; // пробел
+ case 42: decodedChar = ' '; break; // пробел
+ case 43: decodedChar = ' '; break; // пробел
+ case 44: decodedChar = ' '; break; // пробел
+ case 45: decodedChar = ' '; break; // пробел
+ case 46: decodedChar = ' '; break; // пробел
+ case 47: decodedChar = ' '; break; // пробел
+ case 48: decodedChar = '0'; break; // пробел
+ case 49: decodedChar = '1'; break; // пробел
+ case 50: decodedChar = '2'; break; // пробел
+ case 51: decodedChar = '3'; break; // пробел
+ case 52: decodedChar = '4'; break; // пробел
+ case 53: decodedChar = '5'; break; // пробел
+ case 54: decodedChar = '6'; break; // пробел
+ case 55: decodedChar = '7'; break; // пробел
+ case 56: decodedChar = '8'; break; // пробел
+ case 57: decodedChar = '9'; break; // пробел
+ case 58: decodedChar = ' '; break; // пробел
+ case 59: decodedChar = ' '; break; // пробел
+ case 60: decodedChar = ' '; break; // пробел
+ case 61: decodedChar = ' '; break; // пробел
+ case 62: decodedChar = ' '; break; // пробел
+ case 63: decodedChar = ' '; break; // пробел
+ default: decodedChar = ' '; break; // на всякий случай
+ }
+
+ Log.d(TAG, "Символ " + (i/6 + 1) + ": биты=" + charBits + ", значение=" + value + ", символ='" + decodedChar + "'");
+ result.append(decodedChar);
+ }
+
+ String resultStr = result.toString().trim();
+ Log.d(TAG, "Результат декодирования: '" + resultStr + "'");
+ return resultStr;
+ }
+
+ /**
+ * Получает навигационный статус по коду
+ */
+ private String getNavigationStatus(int status) {
+ switch (status) {
+ case 0: return "Under way using engine";
+ case 1: return "At anchor";
+ case 2: return "Not under command";
+ case 3: return "Restricted manoeuvrability";
+ case 4: return "Constrained by her draught";
+ case 5: return "Moored";
+ case 6: return "Aground";
+ case 7: return "Engaged in fishing";
+ case 8: return "Under way sailing";
+ case 9: return "Reserved";
+ case 10: return "Reserved";
+ case 11: return "Reserved";
+ case 12: return "Reserved";
+ case 13: return "Reserved";
+ case 14: return "AIS-SART";
+ case 15: return "Not defined";
+ default: return "Unknown";
+ }
+ }
+
+ /**
+ * Получает описание типа электронного устройства позиционирования
+ */
+ private String getEPFDType(int epfdType) {
+ switch (epfdType) {
+ case 0: return "Undefined";
+ case 1: return "GPS";
+ case 2: return "GLONASS";
+ case 3: return "Combined GPS/GLONASS";
+ case 4: return "Loran-C";
+ case 5: return "Chayka";
+ case 6: return "Integrated navigation system";
+ case 7: return "Surveyed";
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15: return "Not used";
+ default: return "Unknown";
+ }
+ }
+
+ /**
+ * Получает тип судна по коду согласно стандарту AIS
+ */
+ private String getVesselType(int typeCode) {
+ switch (typeCode) {
+ case 0: return "Not available";
+ case 1:
+ case 2:
+ case 3:
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ case 10:
+ case 11:
+ case 12:
+ case 13:
+ case 14:
+ case 15:
+ case 16:
+ case 17:
+ case 18:
+ case 19: return "Reserved for future use";
+ case 20: return "Wing in ground (WIG), all ships";
+ case 21: return "Wing in ground (WIG), Hazardous category A";
+ case 22: return "Wing in ground (WIG), Hazardous category B";
+ case 23: return "Wing in ground (WIG), Hazardous category C";
+ case 24: return "Wing in ground (WIG), Hazardous category D";
+ case 25:
+ case 26:
+ case 27:
+ case 28:
+ case 29: return "Wing in ground (WIG), Reserved";
+ case 30: return "Fishing";
+ case 31: return "Towing";
+ case 32: return "Towing: length exceeds 200m or breadth exceeds 25m";
+ case 33: return "Dredging or underwater ops";
+ case 34: return "Diving ops";
+ case 35: return "Military ops";
+ case 36: return "Sailing";
+ case 37: return "Pleasure Craft";
+ case 38:
+ case 39: return "Reserved";
+ case 40: return "High speed craft (HSC), all ships";
+ case 41: return "High speed craft (HSC), Hazardous category A";
+ case 42: return "High speed craft (HSC), Hazardous category B";
+ case 43: return "High speed craft (HSC), Hazardous category C";
+ case 44: return "High speed craft (HSC), Hazardous category D";
+ case 45:
+ case 46:
+ case 47:
+ case 48: return "High speed craft (HSC), Reserved";
+ case 49: return "High speed craft (HSC), No additional information";
+ case 50: return "Pilot Vessel";
+ case 51: return "Search and Rescue vessel";
+ case 52: return "Tug";
+ case 53: return "Port Tender";
+ case 54: return "Anti-pollution equipment";
+ case 55: return "Law Enforcement";
+ case 56:
+ case 57: return "Spare - Local Vessel";
+ case 58: return "Medical Transport";
+ case 59: return "Noncombatant ship according to RR Resolution No. 18";
+ case 60: return "Passenger, all ships";
+ case 61: return "Passenger, Hazardous category A";
+ case 62: return "Passenger, Hazardous category B";
+ case 63: return "Passenger, Hazardous category C";
+ case 64: return "Passenger, Hazardous category D";
+ case 65:
+ case 66:
+ case 67:
+ case 68: return "Passenger, Reserved";
+ case 69: return "Passenger, No additional information";
+ case 70: return "Cargo, all ships";
+ case 71: return "Cargo, Hazardous category A";
+ case 72: return "Cargo, Hazardous category B";
+ case 73: return "Cargo, Hazardous category C";
+ case 74: return "Cargo, Hazardous category D";
+ case 75:
+ case 76:
+ case 77:
+ case 78: return "Cargo, Reserved";
+ case 79: return "Cargo, No additional information";
+ case 80: return "Tanker, all ships";
+ case 81: return "Tanker, Hazardous category A";
+ case 82: return "Tanker, Hazardous category B";
+ case 83: return "Tanker, Hazardous category C";
+ case 84: return "Tanker, Hazardous category D";
+ case 85:
+ case 86:
+ case 87:
+ case 88: return "Tanker, Reserved";
+ case 89: return "Tanker, No additional information";
+ case 90: return "Other Type, all ships";
+ case 91: return "Other Type, Hazardous category A";
+ case 92: return "Other Type, Hazardous category B";
+ case 93: return "Other Type, Hazardous category C";
+ case 94: return "Other Type, Hazardous category D";
+ case 95:
+ case 96:
+ case 97:
+ case 98: return "Other Type, Reserved";
+ case 99: return "Other Type, no additional information";
+ default: return "Unknown";
+ }
+ }
+
+ /**
+ * Находит существующее AIS судно или создает новое
+ */
+ private AISVessel findOrCreateAISVessel(String mmsi) {
+ for (AISVessel vessel : aisVessels) {
+ if (mmsi.equals(vessel.getMmsi())) {
+ return vessel;
+ }
+ }
+
+ // Создаем новое судно
+ AISVessel newVessel = new AISVessel(mmsi);
+ aisVessels.add(newVessel);
+ Log.d(TAG, "Создано новое AIS судно: " + mmsi);
+ return newVessel;
+ }
+
+ /**
+ * Очищает устаревшие AIS суда (данные старше 10 минут)
+ */
+ public void cleanupStaleAISVessels() {
+ java.util.Iterator iterator = aisVessels.iterator();
+ int removedCount = 0;
+
+ while (iterator.hasNext()) {
+ AISVessel vessel = iterator.next();
+ if (vessel.isDataStale()) {
+ iterator.remove();
+ removedCount++;
+ Log.d(TAG, "Удалено устаревшее AIS судно: " + vessel.getMmsi());
+ }
+ }
+
+ if (removedCount > 0) {
+ Log.i(TAG, "Удалено " + removedCount + " устаревших AIS судов");
+ }
+ }
+
+ /**
+ * Получает количество активных AIS судов
+ */
+ public int getActiveAISVesselCount() {
+ cleanupStaleAISVessels();
+ return aisVessels.size();
+ }
+
+ /**
+ * Получает AIS судно по MMSI
+ */
+ public AISVessel getAISVesselByMMSI(String mmsi) {
+ for (AISVessel vessel : aisVessels) {
+ if (mmsi.equals(vessel.getMmsi())) {
+ return vessel;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Обновляет статус активности AIS судов
+ */
+ public void updateAISVesselActivity() {
+ long currentTime = System.currentTimeMillis();
+ for (AISVessel vessel : aisVessels) {
+ // Считаем судно активным, если данные получены менее 5 минут назад
+ boolean isActive = (currentTime - vessel.getLastUpdate().toInstant(java.time.ZoneOffset.UTC).toEpochMilli()) < 300000;
+ vessel.setActive(isActive);
+ }
+ }
+
+ /**
+ * Парсит координаты из NMEA формата
+ */
+ private double parseCoordinate(String coordinate, boolean isPositive) {
+ // Проверяем, что координата не пустая
+ if (coordinate == null || coordinate.trim().isEmpty()) {
+ return 0.0;
+ }
+
+ try {
+ double value = Double.parseDouble(coordinate);
+ int degrees = (int) (value / 100);
+ double minutes = value - (degrees * 100);
+ double result = degrees + (minutes / 60.0);
+ return isPositive ? result : -result;
+ } catch (NumberFormatException e) {
+ Log.w(TAG, "Ошибка парсинга координаты: " + coordinate + ", ошибка: " + e.getMessage());
+ return 0.0;
+ }
+ }
+
+ /**
+ * Проверяет контрольную сумму NMEA сообщения
+ */
+ public boolean validateChecksum(String nmeaSentence) {
+ if (nmeaSentence == null || !nmeaSentence.contains("*")) {
+ return false;
+ }
+
+ int asteriskIndex = nmeaSentence.indexOf('*');
+ String sentence = nmeaSentence.substring(1, asteriskIndex);
+ String checksum = nmeaSentence.substring(asteriskIndex + 1);
+
+ int calculatedChecksum = 0;
+ for (char c : sentence.toCharArray()) {
+ calculatedChecksum ^= c;
+ }
+
+ String hexChecksum = String.format("%02X", calculatedChecksum);
+ return hexChecksum.equals(checksum);
+ }
+
+ public Vessel getOwnVessel() {
+ return ownVessel;
+ }
+
+ public List getAISVessels() {
+ return new ArrayList<>(aisVessels);
+ }
+
+ /**
+ * Получает количество спутников GPS
+ */
+ public int getGPSSatellites() {
+ return gpsSatellites;
+ }
+
+ /**
+ * Получает количество спутников GLONASS
+ */
+ public int getGLONASSSatellites() {
+ return glonassSatellites;
+ }
+
+ /**
+ * Получает количество спутников Galileo
+ */
+ public int getGalileoSatellites() {
+ return galileoSatellites;
+ }
+
+ /**
+ * Получает общее количество спутников всех систем
+ */
+ public int getTotalSatellites() {
+ return gpsSatellites + glonassSatellites + galileoSatellites;
+ }
+
+ /**
+ * Сбрасывает счетчики спутников
+ */
+ public void resetSatelliteCounters() {
+ gpsSatellites = 0;
+ glonassSatellites = 0;
+ galileoSatellites = 0;
+ ownVessel.setSatellites(0);
+ Log.d(TAG, "Счетчики спутников сброшены");
+ }
+
+ /**
+ * Синхронизирует данные о спутниках с GPSLocationListener
+ */
+ public void syncSatelliteData() {
+ if (gpsLocationListener != null) {
+ gpsLocationListener.setSatellitesInVessel(ownVessel);
+ }
+ }
+
+ /**
+ * Получает текущее состояние объекта Vessel
+ */
+ public String getVesselStatus() {
+ return String.format("Vessel: satellites=%d, activeSatellites=%d, GPS=%d, GLONASS=%d, Galileo=%d",
+ ownVessel.getSatellites(), ownVessel.getActiveSatellites(),
+ gpsSatellites, glonassSatellites, galileoSatellites);
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 4 (Base Station Report)
+ */
+ private void decodeBaseStationReport(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Base Station Report, payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Year (14 бит) - бит 38
+ String yearBits = decodeAISField(payload, 38, 14);
+ int year = Integer.parseInt(yearBits, 2);
+ Log.d(TAG, "Year bits: " + yearBits + " = " + year);
+
+ // Month (4 бита) - бит 52
+ String monthBits = decodeAISField(payload, 52, 4);
+ int month = Integer.parseInt(monthBits, 2);
+ Log.d(TAG, "Month bits: " + monthBits + " = " + month);
+
+ // Day (5 бит) - бит 56
+ String dayBits = decodeAISField(payload, 56, 5);
+ int day = Integer.parseInt(dayBits, 2);
+ Log.d(TAG, "Day bits: " + dayBits + " = " + day);
+
+ // Hour (5 бит) - бит 61
+ String hourBits = decodeAISField(payload, 61, 5);
+ int hour = Integer.parseInt(hourBits, 2);
+ Log.d(TAG, "Hour bits: " + hourBits + " = " + hour);
+
+ // Minute (6 бит) - бит 66
+ String minuteBits = decodeAISField(payload, 66, 6);
+ int minute = Integer.parseInt(minuteBits, 2);
+ Log.d(TAG, "Minute bits: " + minuteBits + " = " + minute);
+
+ // Second (6 бит) - бит 72
+ String secondBits = decodeAISField(payload, 72, 6);
+ int second = Integer.parseInt(secondBits, 2);
+ Log.d(TAG, "Second bits: " + secondBits + " = " + second);
+
+ // Position Accuracy (1 бит) - бит 78
+ String accuracyBits = decodeAISField(payload, 78, 1);
+ int accuracy = Integer.parseInt(accuracyBits, 2);
+ Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
+
+ // Longitude (28 бит) - бит 79
+ String lonBits = decodeAISField(payload, 79, 28);
+ double longitude = parseAISCoordinate(lonBits, 28);
+ Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
+
+ // Latitude (27 бит) - бит 107
+ String latBits = decodeAISField(payload, 107, 27);
+ double latitude = parseAISCoordinate(latBits, 27);
+ Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
+
+ // EPFD Type (4 бита) - бит 134
+ String epfdBits = decodeAISField(payload, 134, 4);
+ int epfdType = Integer.parseInt(epfdBits, 2);
+ Log.d(TAG, "EPFD Type bits: " + epfdBits + " = " + epfdType);
+
+ Log.d(TAG, String.format("AIS Base Station: MMSI=%d, date=%04d-%02d-%02d %02d:%02d:%02d, lat=%.6f, lon=%.6f, accuracy=%d, epfd=%d",
+ mmsi, year, month, day, hour, minute, second, latitude, longitude, accuracy, epfdType));
+
+ // Создаем или обновляем AIS судно (базовая станция)
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.updatePosition(latitude, longitude, 0.0, 0.0);
+ vessel.setPositionAccuracy(accuracy == 1);
+ vessel.setVesselClass("Base Station");
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Base Station Report: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 14 (Safety Related Broadcast Message)
+ */
+ private void decodeSafetyBroadcast(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Safety Broadcast, payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Spare (2 бита) - бит 38
+ String spareBits = decodeAISField(payload, 38, 2);
+ int spare = Integer.parseInt(spareBits, 2);
+ Log.d(TAG, "Spare bits: " + spareBits + " = " + spare);
+
+ // Text (120 бит) - бит 40
+ String textBits = decodeAISField(payload, 40, 120);
+ String safetyText = decodeAISString(textBits);
+ Log.d(TAG, "Safety Text bits: " + textBits + " = '" + safetyText + "'");
+
+ Log.d(TAG, String.format("AIS Safety Broadcast: MMSI=%d, text='%s'", mmsi, safetyText));
+
+ // Создаем или обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.setLastSafetyMessage(safetyText);
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Safety Broadcast: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 18 (Standard Class B Equipment Position Report)
+ */
+ private void decodeClassBPositionReport(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Speed Over Ground (10 бит) - бит 46
+ String speedBits = decodeAISField(payload, 46, 10);
+ double speed = Integer.parseInt(speedBits, 2) / 10.0;
+ Log.d(TAG, "Speed bits: " + speedBits + " = " + speed);
+
+ // Position Accuracy (1 бит) - бит 56
+ String accuracyBits = decodeAISField(payload, 56, 1);
+ int accuracy = Integer.parseInt(accuracyBits, 2);
+ Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
+
+ // Longitude (28 бит) - бит 57
+ String lonBits = decodeAISField(payload, 57, 28);
+ double longitude = parseAISCoordinate(lonBits, 28);
+ Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
+
+ // Latitude (27 бит) - бит 85
+ String latBits = decodeAISField(payload, 85, 27);
+ double latitude = parseAISCoordinate(latBits, 27);
+ Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
+
+ // Course Over Ground (12 бит) - бит 112
+ String courseBits = decodeAISField(payload, 112, 12);
+ double course = Integer.parseInt(courseBits, 2) / 10.0;
+ Log.d(TAG, "Course bits: " + courseBits + " = " + course);
+
+ // True Heading (9 бит) - бит 124
+ String headingBits = decodeAISField(payload, 124, 9);
+ double heading = Integer.parseInt(headingBits, 2);
+ Log.d(TAG, "Heading bits: " + headingBits + " = " + heading);
+
+ // Time Stamp (6 бит) - бит 133
+ String timestampBits = decodeAISField(payload, 133, 6);
+ int timestamp = Integer.parseInt(timestampBits, 2);
+ Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp);
+
+ // Regional Reserved (2 бита) - бит 139
+ String regionalBits = decodeAISField(payload, 139, 2);
+ int regional = Integer.parseInt(regionalBits, 2);
+ Log.d(TAG, "Regional bits: " + regionalBits + " = " + regional);
+
+ // Spare (3 бита) - бит 141
+ String spareBits = decodeAISField(payload, 141, 3);
+ int spare = Integer.parseInt(spareBits, 2);
+ Log.d(TAG, "Spare bits: " + spareBits + " = " + spare);
+
+ Log.d(TAG, String.format("AIS Class B Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f",
+ mmsi, latitude, longitude, course, speed, heading));
+
+ // Создаем или обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.updatePosition(latitude, longitude, course, speed);
+ vessel.setHeading(heading);
+ vessel.setPositionAccuracy(accuracy == 1);
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+ vessel.setVesselClass("Class B");
+
+ // В Class B Position Report размеры не передаются, но мы сохраняем существующие
+ Log.d(TAG, "Class B Position Report - размеры не передаются, сохраняем существующие: L=" + vessel.getLength() + ", W=" + vessel.getWidth());
+
+ // Отправляем информацию о корабле на внешний ресурс
+ String vesselInfo = String.format("Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
+ latitude, longitude, course, speed, heading, accuracy == 1 ? "high" : "low");
+ LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Class B Position Report: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 19 (Extended Class B Equipment Position Report)
+ */
+ private void decodeExtendedClassBPositionReport(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Extended Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // Проверяем длину payload - для Extended Class B должно быть достаточно битов
+ int totalBits = payload.length() * 6;
+ Log.d(TAG, "Общая длина payload в битах: " + totalBits);
+
+ if (totalBits < 312) { // Минимум для Extended Class B
+ Log.w(TAG, "Extended Class B payload слишком короткий: " + totalBits + " бит, ожидается минимум 312");
+ return;
+ }
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Speed Over Ground (10 бит) - бит 46
+ String speedBits = decodeAISField(payload, 46, 10);
+ double speed = Integer.parseInt(speedBits, 2) / 10.0;
+ Log.d(TAG, "Speed bits: " + speedBits + " = " + speed);
+
+ // Position Accuracy (1 бит) - бит 56
+ String accuracyBits = decodeAISField(payload, 56, 1);
+ int accuracy = Integer.parseInt(accuracyBits, 2);
+ Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
+
+ // Longitude (28 бит) - бит 57
+ String lonBits = decodeAISField(payload, 57, 28);
+ double longitude = parseAISCoordinate(lonBits, 28);
+ Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
+
+ // Latitude (27 бит) - бит 85
+ String latBits = decodeAISField(payload, 85, 27);
+ double latitude = parseAISCoordinate(latBits, 27);
+ Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
+
+ // Course Over Ground (12 бит) - бит 112
+ String courseBits = decodeAISField(payload, 112, 12);
+ double course = Integer.parseInt(courseBits, 2) / 10.0;
+ Log.d(TAG, "Course bits: " + courseBits + " = " + course);
+
+ // True Heading (9 бит) - бит 124
+ String headingBits = decodeAISField(payload, 124, 9);
+ double heading = Integer.parseInt(headingBits, 2);
+ Log.d(TAG, "Heading bits: " + headingBits + " = " + heading);
+
+ // Time Stamp (6 бит) - бит 133
+ String timestampBits = decodeAISField(payload, 133, 6);
+ int timestamp = Integer.parseInt(timestampBits, 2);
+ Log.d(TAG, "Timestamp bits: " + timestampBits + " = " + timestamp);
+
+ // Regional Reserved (4 бита) - бит 139
+ String regionalBits = decodeAISField(payload, 139, 4);
+ int regional = Integer.parseInt(regionalBits, 2);
+ Log.d(TAG, "Regional bits: " + regionalBits + " = " + regional);
+
+ // Vessel Name (120 бит) - бит 143
+ String nameBits = decodeAISField(payload, 143, 120);
+ String vesselName = decodeAISString(nameBits);
+ Log.d(TAG, "Name bits: " + nameBits + " = '" + vesselName + "'");
+
+ // Ship Type (8 бит) - бит 263
+ String typeBits = decodeAISField(payload, 263, 8);
+ int vesselTypeCode = Integer.parseInt(typeBits, 2);
+ Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode);
+
+ // Dimension Reference (4 бита) - бит 271
+ String dimRefABits = decodeAISField(payload, 271, 4);
+ String dimRefBBits = decodeAISField(payload, 275, 4);
+ String dimRefCBits = decodeAISField(payload, 279, 4);
+ String dimRefDBits = decodeAISField(payload, 283, 4);
+
+ int dimRefA = Integer.parseInt(dimRefABits, 2);
+ int dimRefB = Integer.parseInt(dimRefBBits, 2);
+ int dimRefC = Integer.parseInt(dimRefCBits, 2);
+ int dimRefD = Integer.parseInt(dimRefDBits, 2);
+
+ Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD);
+
+ // Vessel Dimensions (40 бит) - начинаются с бита 287
+ // Проверяем, есть ли достаточно битов для размеров
+ if (totalBits < 327) {
+ Log.w(TAG, "Extended Class B - недостаточно битов для размеров: " + totalBits + " < 327");
+ // Создаем судно без размеров
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.updatePosition(latitude, longitude, course, speed);
+ vessel.setHeading(heading);
+ vessel.setPositionAccuracy(accuracy == 1);
+ vessel.setVesselName(vesselName);
+ vessel.setVesselType(getVesselType(vesselTypeCode));
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+ vessel.setVesselClass("Extended Class B");
+
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+ return;
+ }
+
+ // Dim.A (10 бит) - от носа до антенны
+ String dimABits = decodeAISField(payload, 287, 10);
+ // Dim.B (10 бит) - от антенны до кормы
+ String dimBBits = decodeAISField(payload, 297, 10);
+ // Dim.C (10 бит) - от левого борта до антенны
+ String dimCBits = decodeAISField(payload, 307, 10);
+ // Dim.D (10 бит) - от антенны до правого борта
+ String dimDBits = decodeAISField(payload, 317, 10);
+
+ Log.d(TAG, "Raw dimension bits - Dim.A: " + dimABits + ", Dim.B: " + dimBBits + ", Dim.C: " + dimCBits + ", Dim.D: " + dimDBits);
+
+ int dimA = Integer.parseInt(dimABits, 2);
+ int dimB = Integer.parseInt(dimBBits, 2);
+ int dimC = Integer.parseInt(dimCBits, 2);
+ int dimD = Integer.parseInt(dimDBits, 2);
+
+ // В AIS стандарте размеры кодируются как 6-битные значения:
+ // 0 = не указано, 1-62 = размер в метрах, 63 = размер 63+ метра
+ // Но мы получаем 10-битные значения, поэтому нужно их правильно интерпретировать
+
+ // Проверяем, что размеры в разумных пределах (0-1000 метров)
+ if (dimA > 1000 || dimB > 1000 || dimC > 1000 || dimD > 1000) {
+ Log.w(TAG, "Размеры судна выходят за разумные пределы: A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
+ // Возможно, мы неправильно интерпретируем битовые поля
+ // Попробуем интерпретировать как 6-битные значения
+ dimA = dimA & 0x3F; // Берем только младшие 6 бит
+ dimB = dimB & 0x3F;
+ dimC = dimC & 0x3F;
+ dimD = dimD & 0x3F;
+ Log.d(TAG, "Исправленные размеры (6-битные): A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
+ }
+
+ // Дополнительная проверка: если размеры все еще неразумные, используем Dimension Reference
+ if (dimA > 100 || dimB > 100 || dimC > 100 || dimD > 100) {
+ Log.w(TAG, "Размеры все еще неразумные, используем Dimension Reference: A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
+ // Используем Dimension Reference как fallback
+ dimA = dimRefA;
+ dimB = dimRefB;
+ dimC = dimRefC;
+ dimD = dimRefD;
+ Log.d(TAG, "Fallback размеры из Dimension Reference: A=" + dimA + ", B=" + dimB + ", C=" + dimC + ", D=" + dimD);
+ }
+
+ // Размеры судна рассчитываются как:
+ // Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
+ // Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
+ double length = dimA + dimB;
+ double width = dimC + dimD;
+
+ Log.d(TAG, "Dimensions - Dim.A (нос-антенна): " + dimABits + " = " + dimA);
+ Log.d(TAG, "Dimensions - Dim.B (антенна-корма): " + dimBBits + " = " + dimB);
+ Log.d(TAG, "Dimensions - Dim.C (левый борт-антенна): " + dimCBits + " = " + dimC);
+ Log.d(TAG, "Dimensions - Dim.D (антенна-правый борт): " + dimDBits + " = " + dimD);
+ Log.d(TAG, "Dimensions - Total Length (A+B): " + length + "m");
+ Log.d(TAG, "Dimensions - Total Width (C+D): " + width + "m");
+
+ Log.d(TAG, String.format("AIS Extended Class B: MMSI=%d, name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, type=%d, L=%.1f, W=%.1f",
+ mmsi, vesselName, latitude, longitude, course, speed, vesselTypeCode, length, width));
+
+ // Создаем или обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.updatePosition(latitude, longitude, course, speed);
+ vessel.setHeading(heading);
+ vessel.setPositionAccuracy(accuracy == 1);
+ vessel.setVesselName(vesselName);
+ vessel.setVesselType(getVesselType(vesselTypeCode));
+ vessel.setLength(length);
+ vessel.setWidth(width);
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+ vessel.setVesselClass("Extended Class B");
+
+ // Отправляем информацию о корабле на внешний ресурс
+ String vesselInfo = String.format("Extended Class B: name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, type=%s, L=%.1f, W=%.1f",
+ vesselName, latitude, longitude, course, speed, getVesselType(vesselTypeCode), length, width);
+ LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Extended Class B Position Report: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 21 (Aid-to-Navigation Report)
+ */
+ private void decodeAidToNavigationReport(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Aid-to-Navigation Report, payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Aid Type (5 бит) - бит 38
+ String aidTypeBits = decodeAISField(payload, 38, 5);
+ int aidType = Integer.parseInt(aidTypeBits, 2);
+ Log.d(TAG, "Aid Type bits: " + aidTypeBits + " = " + aidType);
+
+ // Name (120 бит) - бит 43
+ String nameBits = decodeAISField(payload, 43, 120);
+ String aidName = decodeAISString(nameBits);
+ Log.d(TAG, "Name bits: " + nameBits + " = '" + aidName + "'");
+
+ // Position Accuracy (1 бит) - бит 163
+ String accuracyBits = decodeAISField(payload, 163, 1);
+ int accuracy = Integer.parseInt(accuracyBits, 2);
+ Log.d(TAG, "Accuracy bits: " + accuracyBits + " = " + accuracy);
+
+ // Longitude (28 бит) - бит 164
+ String lonBits = decodeAISField(payload, 164, 28);
+ double longitude = parseAISCoordinate(lonBits, 28);
+ Log.d(TAG, "Longitude bits: " + lonBits + " = " + longitude);
+
+ // Latitude (27 бит) - бит 192
+ String latBits = decodeAISField(payload, 192, 27);
+ double latitude = parseAISCoordinate(latBits, 27);
+ Log.d(TAG, "Latitude bits: " + latBits + " = " + latitude);
+
+ // Dimension Reference (4 бита) - бит 219
+ String dimRefABits = decodeAISField(payload, 219, 4);
+ String dimRefBBits = decodeAISField(payload, 223, 4);
+ String dimRefCBits = decodeAISField(payload, 227, 4);
+ String dimRefDBits = decodeAISField(payload, 231, 4);
+
+ int dimRefA = Integer.parseInt(dimRefABits, 2);
+ int dimRefB = Integer.parseInt(dimRefBBits, 2);
+ int dimRefC = Integer.parseInt(dimRefCBits, 2);
+ int dimRefD = Integer.parseInt(dimRefDBits, 2);
+
+ // Vessel Dimensions (30 бит) - бит 235
+ // Dim.A (10 бит) - от носа до антенны
+ String dimABits = decodeAISField(payload, 235, 10);
+ // Dim.B (10 бит) - от антенны до кормы
+ String dimBBits = decodeAISField(payload, 245, 10);
+ // Dim.C (10 бит) - от левого борта до антенны
+ String dimCBits = decodeAISField(payload, 255, 10);
+ // Dim.D (10 бит) - от антенны до правого борта
+ String dimDBits = decodeAISField(payload, 265, 10);
+ // Draft (8 бит) - осадка
+ String draftBits = decodeAISField(payload, 275, 8);
+
+ int dimA = Integer.parseInt(dimABits, 2);
+ int dimB = Integer.parseInt(dimBBits, 2);
+ int dimC = Integer.parseInt(dimCBits, 2);
+ int dimD = Integer.parseInt(dimDBits, 2);
+
+ // Размеры судна рассчитываются как:
+ // Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
+ // Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
+ double length = dimA + dimB;
+ double width = dimC + dimD;
+ double draft = Integer.parseInt(draftBits, 2) / 10.0;
+
+ Log.d(TAG, String.format("AIS Aid-to-Navigation: MMSI=%d, type=%d, name='%s', lat=%.6f, lon=%.6f, L=%.1f, W=%.1f, D=%.1f",
+ mmsi, aidType, aidName, latitude, longitude, length, width, draft));
+
+ // Создаем или обновляем AIS судно (навигационный знак)
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.updatePosition(latitude, longitude, 0.0, 0.0);
+ vessel.setPositionAccuracy(accuracy == 1);
+ vessel.setVesselName(aidName);
+ vessel.setVesselType("Aid-to-Navigation");
+ vessel.setLength(length);
+ vessel.setWidth(width);
+ vessel.setDraft(draft);
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+ vessel.setVesselClass("Navigation Aid");
+
+ // Уведомляем слушателя
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Aid-to-Navigation Report: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Декодирует AIS сообщение типа 24 (Static Data Report)
+ */
+ private void decodeStaticDataReport(String payload) {
+ try {
+ Log.d(TAG, "Декодируем Static Data Report, payload: " + payload + " (длина: " + payload.length() + ")");
+
+ // MMSI (30 бит) - начинается с бита 8
+ String mmsiBits = decodeAISField(payload, 8, 30);
+ int mmsi = Integer.parseInt(mmsiBits, 2);
+ Log.d(TAG, "MMSI bits: " + mmsiBits + " = " + mmsi);
+
+ // Part Number (2 бита) - бит 38
+ String partBits = decodeAISField(payload, 38, 2);
+ int partNumber = Integer.parseInt(partBits, 2);
+ Log.d(TAG, "Part Number bits: " + partBits + " = " + partNumber);
+
+ if (partNumber == 0) {
+ // Part A: Vessel Name
+ String nameBits = decodeAISField(payload, 40, 120);
+ String vesselName = decodeAISString(nameBits);
+ Log.d(TAG, "Vessel Name bits: " + nameBits + " = '" + vesselName + "'");
+
+ Log.d(TAG, String.format("AIS Static Data Part A: MMSI=%d, name='%s'", mmsi, vesselName));
+
+ // Обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.setVesselName(vesselName);
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+
+ } else if (partNumber == 1) {
+ // Part B: Vessel Type, Dimensions, etc.
+ String typeBits = decodeAISField(payload, 40, 8);
+ int vesselTypeCode = Integer.parseInt(typeBits, 2);
+ Log.d(TAG, "Vessel Type bits: " + typeBits + " = " + vesselTypeCode);
+
+ // Vendor ID (42 бита) - бит 48
+ String vendorBits = decodeAISField(payload, 48, 42);
+ String vendorId = decodeAISString(vendorBits);
+ Log.d(TAG, "Vendor ID bits: " + vendorBits + " = '" + vendorId + "'");
+
+ // Call Sign (42 бита) - бит 90
+ String callSignBits = decodeAISField(payload, 90, 42);
+ String callSign = decodeAISString(callSignBits);
+ Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'");
+
+ // Dimension Reference (6 бит каждое) - бит 132
+ // Согласно онлайн декодеру, размеры находятся в других позициях
+ // Попробуем позиции, которые соответствуют онлайн декодеру
+ String dimRefABits = decodeAISField(payload, 132, 9);
+ String dimRefBBits = decodeAISField(payload, 141, 9);
+ String dimRefCBits = decodeAISField(payload, 150, 6);
+ String dimRefDBits = decodeAISField(payload, 156, 6);
+
+ int dimRefA = Integer.parseInt(dimRefABits, 2);
+ int dimRefB = Integer.parseInt(dimRefBBits, 2);
+ int dimRefC = Integer.parseInt(dimRefCBits, 2);
+ int dimRefD = Integer.parseInt(dimRefDBits, 2);
+
+ Log.d(TAG, "Dimension Reference bits - A: " + dimRefABits + " = " + dimRefA);
+ Log.d(TAG, "Dimension Reference bits - B: " + dimRefBBits + " = " + dimRefB);
+ Log.d(TAG, "Dimension Reference bits - C: " + dimRefCBits + " = " + dimRefC);
+ Log.d(TAG, "Dimension Reference bits - D: " + dimRefDBits + " = " + dimRefD);
+
+ // Проверяем, есть ли достаточно битов для размеров
+ int totalBits = payload.length() * 6;
+ Log.d(TAG, "Static Data Part B - общая длина payload в битах: " + totalBits);
+
+ double length = 0.0;
+ double width = 0.0;
+ double draft = 0.0;
+
+ // Для коротких сообщений типа 24 Part B (168 бит) используем Dimension Reference
+ // В коротких сообщениях размеры кодируются в Dimension Reference полях
+ if (totalBits >= 168) {
+ // В сообщениях типа 24 Part B для Class B судов
+ // размеры кодируются в полях Dimension Reference (биты 132-147)
+ // где каждое поле - 4 бита и представляет размер в метрах
+ // Эти поля уже правильно декодированы выше
+
+ // Размеры судна рассчитываются как:
+ // Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
+ // Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
+ length = dimRefA + dimRefB;
+ width = dimRefC + dimRefD;
+
+ Log.d(TAG, "Static Data Part B - используем Dimension Reference:");
+ Log.d(TAG, " Dim.A (нос-антенна): " + dimRefA + " м");
+ Log.d(TAG, " Dim.B (антенна-корма): " + dimRefB + " м");
+ Log.d(TAG, " Dim.C (левый борт-антенна): " + dimRefC + " м");
+ Log.d(TAG, " Dim.D (антенна-правый борт): " + dimRefD + " м");
+ Log.d(TAG, "Static Data Part B - итоговые размеры: L=" + length + ", W=" + width);
+
+ } else {
+ Log.w(TAG, "Static Data Part B - недостаточно битов для размеров: " + totalBits + " < 168");
+ // Используем нулевые размеры
+ length = 0.0;
+ width = 0.0;
+ }
+
+ Log.d(TAG, String.format("AIS Static Data Part B: MMSI=%d, type=%d, vendor='%s', callSign='%s', L=%.1f, W=%.1f, D=%.1f",
+ mmsi, vesselTypeCode, vendorId, callSign, length, width, draft));
+
+ // Обновляем AIS судно
+ AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
+ vessel.setVesselType(getVesselType(vesselTypeCode));
+ vessel.setVendorId(vendorId);
+ vessel.setCallSign(callSign);
+ vessel.setLength(length);
+ vessel.setWidth(width);
+ vessel.setDraft(draft);
+ vessel.setLastUpdate(java.time.LocalDateTime.now());
+
+ if (listener != null) {
+ listener.onAISVesselUpdated(vessel);
+ }
+ }
+
+ } catch (Exception e) {
+ Log.e(TAG, "Ошибка декодирования Static Data Report: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/app/build.gradle b/app/build.gradle
index 855f6e0..695945b 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -46,6 +46,13 @@ dependencies {
implementation group: 'org.mapsforge', name: 'mapsforge-map-reader', version: '0.25.0'
implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.25.0'
+ // Room
+ implementation "androidx.room:room-runtime:2.6.1"
+ annotationProcessor "androidx.room:room-compiler:2.6.1"
+ // Lifecycle (для сервисов/репозитория при необходимости)
+ implementation 'androidx.lifecycle:lifecycle-runtime:2.8.3'
+ implementation 'androidx.lifecycle:lifecycle-livedata:2.8.3'
+
// Тестирование
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 816f8b8..e7566c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,11 @@
+
+
+
+
+
@@ -13,6 +18,9 @@
+
+
+
@@ -31,7 +39,7 @@
android:supportsRtl="true"
android:theme="@style/Theme.AISMap"
tools:targetApi="31">
-
+
+
+
+
+
+
(), this);
+ recyclerView.setAdapter(adapter);
+
+ repository.observeAllAIS().observe(this, new Observer>() {
+ @Override
+ public void onChanged(List entities) {
+ // Стабильная сортировка по MMSI для предсказуемого порядка
+ if (entities != null) {
+ java.util.Collections.sort(entities, (a, b) -> a.mmsi.compareTo(b.mmsi));
+ }
+ adapter.submitList(entities);
+
+ // Показываем/скрываем сообщение о пустом состоянии
+ if (entities == null || entities.isEmpty()) {
+ textEmptyState.setVisibility(android.view.View.VISIBLE);
+ recyclerView.setVisibility(android.view.View.GONE);
+ } else {
+ textEmptyState.setVisibility(android.view.View.GONE);
+ recyclerView.setVisibility(android.view.View.VISIBLE);
+ }
+ }
+ });
+
+ // Тикер для обновления поля "N сек назад"
+ tickerHandler = new android.os.Handler(android.os.Looper.getMainLooper());
+ tickerRunnable = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // Обновляем только элементы с данными, чтобы избежать мигания
+ int itemCount = adapter.getItemCount();
+ for (int i = 0; i < itemCount; i++) {
+ adapter.notifyItemChanged(i, "time_update");
+ }
+ } finally {
+ tickerHandler.postDelayed(this, 1000);
+ }
+ }
+ };
+ tickerHandler.postDelayed(tickerRunnable, 1000);
+ }
+
+ @Override
+ public void onMarinetrafficClick(String mmsi) {
+ String url = "https://www.marinetraffic.com/ru/ais/details/ships/mmsi:" + mmsi;
+ Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ startActivity(browserIntent);
+ }
+
+ @Override
+ public void onCenterOnMapClick(String mmsi, double lat, double lon) {
+ Intent intent = new Intent(this, MainActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ intent.putExtra("center_mmsi", mmsi);
+ intent.putExtra("center_lat", lat);
+ intent.putExtra("center_lon", lon);
+ startActivity(intent);
+ finish();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (tickerHandler != null && tickerRunnable != null) {
+ tickerHandler.removeCallbacks(tickerRunnable);
+ }
+ }
+}
+
+
diff --git a/app/src/main/java/com/grigowashere/aismap/AisTargetsAdapter.java b/app/src/main/java/com/grigowashere/aismap/AisTargetsAdapter.java
new file mode 100644
index 0000000..3c95e02
--- /dev/null
+++ b/app/src/main/java/com/grigowashere/aismap/AisTargetsAdapter.java
@@ -0,0 +1,138 @@
+package com.grigowashere.aismap;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.grigowashere.aismap.data.entity.AISVesselEntity;
+
+class AisTargetsAdapter extends ListAdapter {
+
+ interface OnItemClickListener {
+ void onMarinetrafficClick(String mmsi);
+ void onCenterOnMapClick(String mmsi, double lat, double lon);
+ }
+
+ private final OnItemClickListener listener;
+
+ protected AisTargetsAdapter(@NonNull DiffUtil.ItemCallback diffCallback, OnItemClickListener listener) {
+ super(diffCallback);
+ this.listener = listener;
+ }
+
+ public AisTargetsAdapter(java.util.List initial, OnItemClickListener listener) {
+ this(DIFF_CALLBACK, listener);
+ submitList(initial);
+ }
+
+ static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() {
+ @Override
+ public boolean areItemsTheSame(@NonNull AISVesselEntity oldItem, @NonNull AISVesselEntity newItem) {
+ return oldItem.mmsi.equals(newItem.mmsi);
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull AISVesselEntity oldItem, @NonNull AISVesselEntity newItem) {
+ return oldItem.latitude == newItem.latitude &&
+ oldItem.longitude == newItem.longitude &&
+ oldItem.course == newItem.course &&
+ oldItem.speed == newItem.speed &&
+ ((oldItem.vesselName == null && newItem.vesselName == null) || (oldItem.vesselName != null && oldItem.vesselName.equals(newItem.vesselName)));
+ // Не проверяем lastUpdateEpochMs, чтобы избежать мигания при обновлении времени
+ }
+ };
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_ais_target, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+ AISVesselEntity item = getItem(position);
+ holder.bind(item, listener);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull java.util.List