generated from Grigo/AndroidTemplate
41432665ea
- Яндекс/MapForge: правки в менеджерах и обёртках маркеров (улучшена отрисовка/логика) - NMEAParser: корректировки парсинга и стабильности - Модель AISVessel: уточнение полей/логики - Настройки: правки в SettingsActivity и SettingsManager, актуализация AppController - UI: обновлены activity_main, activity_settings, bottom_sheet_ais_vessel; меню main_menu - Ресурсы: добавлен drawable/targetclassa.xml, обновлён drawable/target.xml - Конфигурация: правки AndroidManifest и app/build.gradle - Прочее: изменения в .idea (не влияют на сборку)
2836 lines
148 KiB
Java
2836 lines
148 KiB
Java
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<AISVessel> aisVessels;
|
||
private NMEAParserListener listener;
|
||
private GPSLocationListener gpsLocationListener;
|
||
|
||
// Поля для работы с AIS фрагментами
|
||
private java.util.Map<String, java.util.Map<Integer, String>> aisFragments = new java.util.HashMap<>();
|
||
private java.util.Map<String, Long> 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<Integer, String> 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<java.util.Map.Entry<String, Long>> iterator = aisFragmentTimestamps.entrySet().iterator();
|
||
|
||
while (iterator.hasNext()) {
|
||
java.util.Map.Entry<String, Long> 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<AISVessel> 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<AISVessel> 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);
|
||
}
|
||
}
|
||
}
|