fixed NMEAParser

This commit is contained in:
2025-09-09 14:24:12 +03:00
parent 8d63f9d719
commit bdc0aa3ccf
3 changed files with 508 additions and 73 deletions
@@ -11,6 +11,11 @@ import java.util.ArrayList;
/** /**
* Контроллер для парсинга NMEA сообщений * Контроллер для парсинга NMEA сообщений
* Работает в гибридном режиме: координаты через Location API, остальное через NMEA * Работает в гибридном режиме: координаты через Location API, остальное через NMEA
*
* ВАЖНО: Размеры судна в AIS сообщениях рассчитываются относительно положения антенны:
* - Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
* - Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
* Координаты в AIS указывают положение антенны, а не центра судна.
*/ */
/** /**
@@ -747,35 +752,43 @@ public class NMEAParser {
String messageTypeBits = decodeAISField(payload, 0, 6); String messageTypeBits = decodeAISField(payload, 0, 6);
int messageType = Integer.parseInt(messageTypeBits, 2); int messageType = Integer.parseInt(messageTypeBits, 2);
Log.d(TAG, "Декодируем AIS тип " + messageType + " на канале " + channel); Log.d(TAG, "Декодируем AIS тип " + messageType + " на канале " + channel + " (биты: " + messageTypeBits + ")");
switch (messageType) { switch (messageType) {
case 1: case 1:
case 2: case 2:
case 3: case 3:
// Position Report // Position Report
Log.d(TAG, "Обрабатываем Position Report (тип " + messageType + ")");
decodePositionReport(payload, messageType); decodePositionReport(payload, messageType);
break; break;
case 5: case 5:
// Static Data // Static Data
Log.d(TAG, "Обрабатываем Static Data (тип " + messageType + ")");
decodeStaticData(payload); decodeStaticData(payload);
break; break;
case 4: // Base Station Report case 4: // Base Station Report
Log.d(TAG, "Обрабатываем Base Station Report (тип " + messageType + ")");
decodeBaseStationReport(payload); decodeBaseStationReport(payload);
break; break;
case 14: // Safety Related Broadcast Message case 14: // Safety Related Broadcast Message
Log.d(TAG, "Обрабатываем Safety Broadcast (тип " + messageType + ")");
decodeSafetyBroadcast(payload); decodeSafetyBroadcast(payload);
break; break;
case 18: // Standard Class B Equipment Position Report case 18: // Standard Class B Equipment Position Report
Log.d(TAG, "Обрабатываем Class B Position Report (тип " + messageType + ")");
decodeClassBPositionReport(payload); decodeClassBPositionReport(payload);
break; break;
case 19: // Extended Class B Equipment Position Report case 19: // Extended Class B Equipment Position Report
Log.d(TAG, "Обрабатываем Extended Class B Position Report (тип " + messageType + ")");
decodeExtendedClassBPositionReport(payload); decodeExtendedClassBPositionReport(payload);
break; break;
case 21: // Aid-to-Navigation Report case 21: // Aid-to-Navigation Report
Log.d(TAG, "Обрабатываем Aid-to-Navigation Report (тип " + messageType + ")");
decodeAidToNavigationReport(payload); decodeAidToNavigationReport(payload);
break; break;
case 24: // Static Data Report case 24: // Static Data Report
Log.d(TAG, "Обрабатываем Static Data Report (тип " + messageType + ")");
decodeStaticDataReport(payload); decodeStaticDataReport(payload);
break; break;
default: default:
@@ -892,7 +905,12 @@ public class NMEAParser {
// Вырезаем нужный диапазон битов // Вырезаем нужный диапазон битов
if (startBit + length <= fullBinary.length()) { if (startBit + length <= fullBinary.length()) {
return fullBinary.substring(startBit, startBit + 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 { } else {
Log.w(TAG, Log.w(TAG,
"AIS поле выходит за границы: startBit=" + startBit + "AIS поле выходит за границы: startBit=" + startBit +
@@ -1043,11 +1061,11 @@ public class NMEAParser {
int vesselTypeCode = Integer.parseInt(typeBits, 2); int vesselTypeCode = Integer.parseInt(typeBits, 2);
Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode); Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode);
// Dimension Reference (4 бита) - бит 240 // Dimension Reference (9, 9, 6, 6 бит) - бит 240
String dimRefABits = decodeAISField(payload, 240, 4); String dimRefABits = decodeAISField(payload, 240, 9);
String dimRefBBits = decodeAISField(payload, 244, 4); String dimRefBBits = decodeAISField(payload, 249, 9);
String dimRefCBits = decodeAISField(payload, 248, 4); String dimRefCBits = decodeAISField(payload, 258, 6);
String dimRefDBits = decodeAISField(payload, 252, 4); String dimRefDBits = decodeAISField(payload, 264, 6);
int dimRefA = Integer.parseInt(dimRefABits, 2); int dimRefA = Integer.parseInt(dimRefABits, 2);
int dimRefB = Integer.parseInt(dimRefBBits, 2); int dimRefB = Integer.parseInt(dimRefBBits, 2);
@@ -1056,18 +1074,25 @@ public class NMEAParser {
Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD); Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD);
// Vessel Dimensions (30 бит) - бит 256 // Для сообщения типа 5 используем Dimension Reference поля (9, 9, 6, 6 бит)
String lengthBits = decodeAISField(payload, 256, 10); // Размеры судна рассчитываются как:
String widthBits = decodeAISField(payload, 266, 10); // Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы)
String draftBits = decodeAISField(payload, 276, 8); // Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта)
double length = dimRefA + dimRefB;
double width = dimRefC + dimRefD;
double length = Integer.parseInt(lengthBits, 2); // Draft (8 бит) - осадка - бит 296
double width = Integer.parseInt(widthBits, 2); String draftBits = decodeAISField(payload, 296, 8);
double draft = Integer.parseInt(draftBits, 2) / 10.0; double draft = Integer.parseInt(draftBits, 2) / 10.0;
Log.d(TAG, "Dimensions - Length bits: " + lengthBits + " = " + length); Log.d(TAG, "Static Data - используем Dimension Reference поля (9, 9, 6, 6 бит):");
Log.d(TAG, "Dimensions - Width bits: " + widthBits + " = " + width); Log.d(TAG, " Dim.A (нос-антенна): " + dimRefABits + " = " + dimRefA + " м");
Log.d(TAG, "Dimensions - Draft bits: " + draftBits + " = " + draft); 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 бит) - бит 294 // ETA (20 бит) - бит 294
String etaBits = decodeAISField(payload, 294, 20); String etaBits = decodeAISField(payload, 294, 20);
@@ -1374,31 +1399,112 @@ public class NMEAParser {
} }
/** /**
* Получает тип судна по коду * Получает тип судна по коду согласно стандарту AIS
*/ */
private String getVesselType(int typeCode) { private String getVesselType(int typeCode) {
if (typeCode >= 20 && typeCode <= 29) return "Wing in ground"; switch (typeCode) {
if (typeCode >= 30 && typeCode <= 39) return "Fishing"; case 0: return "Not available";
if (typeCode >= 40 && typeCode <= 49) return "Towing"; case 1:
if (typeCode >= 50 && typeCode <= 59) return "Dredging"; case 2:
if (typeCode >= 60 && typeCode <= 69) return "Diving"; case 3:
if (typeCode >= 70 && typeCode <= 79) return "Military"; case 4:
if (typeCode >= 80 && typeCode <= 89) return "Pleasure"; case 5:
if (typeCode >= 90 && typeCode <= 99) return "High speed"; case 6:
if (typeCode >= 100 && typeCode <= 109) return "Pilot vessel"; case 7:
if (typeCode >= 110 && typeCode <= 119) return "SAR"; case 8:
if (typeCode >= 120 && typeCode <= 129) return "Tug"; case 9:
if (typeCode >= 130 && typeCode <= 139) return "Port tender"; case 10:
if (typeCode >= 140 && typeCode <= 149) return "Anti-pollution"; case 11:
if (typeCode >= 150 && typeCode <= 159) return "Law enforce"; case 12:
if (typeCode >= 160 && typeCode <= 169) return "Spare"; case 13:
if (typeCode >= 170 && typeCode <= 179) return "Medical"; case 14:
if (typeCode >= 180 && typeCode <= 189) return "Special craft"; case 15:
if (typeCode >= 190 && typeCode <= 199) return "Passenger"; case 16:
if (typeCode >= 200 && typeCode <= 209) return "Cargo"; case 17:
if (typeCode >= 210 && typeCode <= 219) return "Tanker"; case 18:
if (typeCode >= 220 && typeCode <= 229) return "Other"; case 19: return "Reserved for future use";
return "Unknown"; 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";
}
} }
/** /**
@@ -1767,6 +1873,9 @@ public class NMEAParser {
vessel.setLastUpdate(java.time.LocalDateTime.now()); vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Class B"); 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", 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"); latitude, longitude, course, speed, heading, accuracy == 1 ? "high" : "low");
@@ -1789,6 +1898,15 @@ public class NMEAParser {
try { try {
Log.d(TAG, "Декодируем Extended Class B Position Report, payload: " + payload + " (длина: " + payload.length() + ")"); 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 // MMSI (30 бит) - начинается с бита 8
String mmsiBits = decodeAISField(payload, 8, 30); String mmsiBits = decodeAISField(payload, 8, 30);
int mmsi = Integer.parseInt(mmsiBits, 2); int mmsi = Integer.parseInt(mmsiBits, 2);
@@ -1855,17 +1973,86 @@ public class NMEAParser {
int dimRefC = Integer.parseInt(dimRefCBits, 2); int dimRefC = Integer.parseInt(dimRefCBits, 2);
int dimRefD = Integer.parseInt(dimRefDBits, 2); int dimRefD = Integer.parseInt(dimRefDBits, 2);
// Vessel Dimensions (30 бит) - бит 287 Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD);
String lengthBits = decodeAISField(payload, 287, 10);
String widthBits = decodeAISField(payload, 297, 10);
String draftBits = decodeAISField(payload, 307, 8);
double length = Integer.parseInt(lengthBits, 2); // Vessel Dimensions (40 бит) - начинаются с бита 287
double width = Integer.parseInt(widthBits, 2); // Проверяем, есть ли достаточно битов для размеров
double draft = Integer.parseInt(draftBits, 2) / 10.0; 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;
}
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, D=%.1f", // Dim.A (10 бит) - от носа до антенны
mmsi, vesselName, latitude, longitude, course, speed, vesselTypeCode, length, width, draft)); 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 судно // Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
@@ -1876,13 +2063,12 @@ public class NMEAParser {
vessel.setVesselType(getVesselType(vesselTypeCode)); vessel.setVesselType(getVesselType(vesselTypeCode));
vessel.setLength(length); vessel.setLength(length);
vessel.setWidth(width); vessel.setWidth(width);
vessel.setDraft(draft);
vessel.setLastUpdate(java.time.LocalDateTime.now()); vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Extended Class B"); 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, D=%.1f", 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, draft); vesselName, latitude, longitude, course, speed, getVesselType(vesselTypeCode), length, width);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo); LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Уведомляем слушателя // Уведомляем слушателя
@@ -1944,12 +2130,27 @@ public class NMEAParser {
int dimRefD = Integer.parseInt(dimRefDBits, 2); int dimRefD = Integer.parseInt(dimRefDBits, 2);
// Vessel Dimensions (30 бит) - бит 235 // Vessel Dimensions (30 бит) - бит 235
String lengthBits = decodeAISField(payload, 235, 10); // Dim.A (10 бит) - от носа до антенны
String widthBits = decodeAISField(payload, 245, 10); String dimABits = decodeAISField(payload, 235, 10);
String draftBits = decodeAISField(payload, 255, 8); // 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);
double length = Integer.parseInt(lengthBits, 2); int dimA = Integer.parseInt(dimABits, 2);
double width = Integer.parseInt(widthBits, 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; 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", 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",
@@ -2027,25 +2228,59 @@ public class NMEAParser {
String callSign = decodeAISString(callSignBits); String callSign = decodeAISString(callSignBits);
Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'"); Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'");
// Dimension Reference (4 бита) - бит 132 // Dimension Reference (6 бит каждое) - бит 132
String dimRefABits = decodeAISField(payload, 132, 4); // Согласно онлайн декодеру, размеры находятся в других позициях
String dimRefBBits = decodeAISField(payload, 136, 4); // Попробуем позиции, которые соответствуют онлайн декодеру
String dimRefCBits = decodeAISField(payload, 140, 4); String dimRefABits = decodeAISField(payload, 132, 9);
String dimRefDBits = decodeAISField(payload, 144, 4); 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 dimRefA = Integer.parseInt(dimRefABits, 2);
int dimRefB = Integer.parseInt(dimRefBBits, 2); int dimRefB = Integer.parseInt(dimRefBBits, 2);
int dimRefC = Integer.parseInt(dimRefCBits, 2); int dimRefC = Integer.parseInt(dimRefCBits, 2);
int dimRefD = Integer.parseInt(dimRefDBits, 2); int dimRefD = Integer.parseInt(dimRefDBits, 2);
// Vessel Dimensions (30 бит) - бит 148 Log.d(TAG, "Dimension Reference bits - A: " + dimRefABits + " = " + dimRefA);
String lengthBits = decodeAISField(payload, 148, 10); Log.d(TAG, "Dimension Reference bits - B: " + dimRefBBits + " = " + dimRefB);
String widthBits = decodeAISField(payload, 158, 10); Log.d(TAG, "Dimension Reference bits - C: " + dimRefCBits + " = " + dimRefC);
String draftBits = decodeAISField(payload, 168, 8); Log.d(TAG, "Dimension Reference bits - D: " + dimRefDBits + " = " + dimRefD);
double length = Integer.parseInt(lengthBits, 2); // Проверяем, есть ли достаточно битов для размеров
double width = Integer.parseInt(widthBits, 2); int totalBits = payload.length() * 6;
double draft = Integer.parseInt(draftBits, 2) / 10.0; 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", 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)); mmsi, vesselTypeCode, vendorId, callSign, length, width, draft));
@@ -92,7 +92,7 @@ public class YandexMarkerWrapper extends MarkerWrapper {
private void createMarker() { private void createMarker() {
try { try {
double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLongitude(); double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLatitude();
double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude(); double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude();
// Сначала создаем иконку // Сначала создаем иконку
@@ -58,11 +58,55 @@ public class LogSender {
message += " | " + vesselInfo; message += " | " + vesselInfo;
} }
// Извлекаем тип судна из vesselInfo и генерируем цвет
// Генерируем уникальный цвет для корабля на основе MMSI
String vesselColor = generateVesselColor(mmsi);
String encodedMessage = encodeForURL(message); String encodedMessage = encodeForURL(message);
String url = BASE_URL + "?ships=" + encodedMessage + "&color=green"; String encodedColor = encodeColorForURL(vesselColor);
String url = BASE_URL + "?ships=" + encodedMessage + "&color=" + encodedColor;
sendGetRequest(url); sendGetRequest(url);
Log.d(TAG, "Ship update лог отправлен: " + message); Log.d(TAG, "Ship update лог отправлен: " + message + " ( " + ", цвет: " + vesselColor + ")");
} catch (Exception e) {
Log.e(TAG, "Ошибка отправки ship update лога: " + e.getMessage(), e);
}
});
}
/**
* Отправляет лог обновления информации о корабле с заданным цветом
* @param mmsi MMSI корабля
* @param vesselInfo Информация о корабле
* @param color Цвет в формате HEX (#RRGGBB) или имя цвета
*/
public static void logShipUpdate(String mmsi, String vesselInfo, String color) {
if (mmsi == null || mmsi.trim().isEmpty()) {
return;
}
executor.execute(() -> {
try {
String message = "MMSI: " + mmsi;
if (vesselInfo != null && !vesselInfo.trim().isEmpty()) {
message += " | " + vesselInfo;
}
// Используем переданный цвет или генерируем на основе типа судна
String vesselColor;
if (color != null && !color.trim().isEmpty()) {
vesselColor = color;
} else {
// Генерируем уникальный цвет для корабля на основе MMSI
vesselColor = generateVesselColor(mmsi);
}
String encodedMessage = encodeForURL(message);
String encodedColor = encodeColorForURL(vesselColor);
String url = BASE_URL + "?ships=" + encodedMessage + "&color=" + encodedColor;
sendGetRequest(url);
Log.d(TAG, "Ship update лог отправлен: " + message + " (цвет: " + vesselColor + ")");
} catch (Exception e) { } catch (Exception e) {
Log.e(TAG, "Ошибка отправки ship update лога: " + e.getMessage(), e); Log.e(TAG, "Ошибка отправки ship update лога: " + e.getMessage(), e);
} }
@@ -97,6 +141,160 @@ public class LogSender {
}); });
} }
/**
* Генерирует уникальный цвет для корабля на основе MMSI (устаревший метод)
* @param mmsi MMSI корабля
* @return HEX цвет в формате #RRGGBB
*/
private static String generateVesselColor(String mmsi) {
try {
// Преобразуем MMSI в число для хеширования
long mmsiValue = Long.parseLong(mmsi);
// Используем хеш-функцию для получения равномерного распределения
int hash = Long.hashCode(mmsiValue);
// Извлекаем RGB компоненты из хеша
int r = (hash & 0xFF0000) >> 16;
int g = (hash & 0x00FF00) >> 8;
int b = hash & 0x0000FF;
// Проверяем, не слишком ли темный цвет (чтобы избежать черного)
int brightness = (r + g + b) / 3;
if (brightness < 100) {
// Если цвет слишком темный, осветляем его
r = Math.min(255, r + 120);
g = Math.min(255, g + 120);
b = Math.min(255, b + 120);
}
// Проверяем, не слишком ли светлый цвет (чтобы избежать белого)
if (brightness > 220) {
// Если цвет слишком светлый, затемняем его
r = Math.max(0, r - 60);
g = Math.max(0, g - 60);
b = Math.max(0, b - 60);
}
// Форматируем в HEX
String color = String.format("#%02X%02X%02X", r, g, b);
Log.d(TAG, "Сгенерирован цвет для MMSI " + mmsi + ": " + color + " (RGB: " + r + "," + g + "," + b + ")");
return color;
} catch (NumberFormatException e) {
Log.w(TAG, "Не удалось распарсить MMSI как число: " + mmsi + ", используем цвет по умолчанию");
return "#00AA00"; // Зеленый по умолчанию
} catch (Exception e) {
Log.e(TAG, "Ошибка генерации цвета для MMSI " + mmsi + ": " + e.getMessage(), e);
return "#00AA00"; // Зеленый по умолчанию
}
}
/**
* Определяет тип судна по MMSI
* Использует более точную логику на основе стандартных диапазонов MMSI
* @param mmsi MMSI судна
* @return Тип судна
*/
private static String getVesselTypeByMMSI(long mmsi) {
// Стандартные диапазоны MMSI для разных типов судов
if (mmsi >= 100000000 && mmsi <= 199999999) {
return "COASTAL"; // Прибрежные суда
} else if (mmsi >= 200000000 && mmsi <= 299999999) {
return "FISHING"; // Рыболовные суда
} else if (mmsi >= 300000000 && mmsi <= 399999999) {
return "CARGO"; // Грузовые суда
} else if (mmsi >= 400000000 && mmsi <= 499999999) {
return "TANKER"; // Танкеры
} else if (mmsi >= 500000000 && mmsi <= 599999999) {
return "PASSENGER"; // Пассажирские суда
} else if (mmsi >= 600000000 && mmsi <= 699999999) {
return "MILITARY"; // Военные корабли
} else if (mmsi >= 700000000 && mmsi <= 799999999) {
return "PILOT"; // Лоцманские суда
} else if (mmsi >= 800000000 && mmsi <= 899999999) {
return "PILOT"; // Лоцманские суда (дополнительный диапазон)
} else if (mmsi >= 900000000 && mmsi <= 999999999) {
return "MILITARY"; // Военные корабли (дополнительный диапазон)
} else if (mmsi >= 1000000000 && mmsi <= 1099999999) {
return "SAR"; // Спасательные суда
} else if (mmsi >= 1100000000 && mmsi <= 1199999999) {
return "TUG"; // Буксиры
} else if (mmsi >= 1200000000 && mmsi <= 1299999999) {
return "PORT_TENDER"; // Портовые суда
} else if (mmsi >= 1300000000 && mmsi <= 1399999999) {
return "ANTI_POLLUTION"; // Антизагрязнительные суда
} else if (mmsi >= 1400000000 && mmsi <= 1499999999) {
return "LAW_ENFORCEMENT"; // Правоохранительные суда
} else if (mmsi >= 1500000000 && mmsi <= 1599999999) {
return "MEDICAL"; // Медицинские суда
} else if (mmsi >= 1600000000 && mmsi <= 1699999999) {
return "SPECIAL_CRAFT"; // Специальные суда
} else if (mmsi >= 1700000000 && mmsi <= 1799999999) {
return "PASSENGER"; // Пассажирские суда (дополнительный диапазон)
} else if (mmsi >= 1800000000 && mmsi <= 1899999999) {
return "CARGO"; // Грузовые суда (дополнительный диапазон)
} else if (mmsi >= 1900000000 && mmsi <= 1999999999) {
return "TANKER"; // Танкеры (дополнительный диапазон)
} else if (mmsi >= 2000000000 && mmsi <= 2099999999) {
return "OTHER"; // Другие типы судов
} else if (mmsi >= 2100000000L && mmsi <= 2199999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2200000000L && mmsi <= 2299999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2300000000L && mmsi <= 2399999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2400000000L && mmsi <= 2499999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2500000000L && mmsi <= 2599999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2600000000L && mmsi <= 2699999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2700000000L && mmsi <= 2799999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2800000000L && mmsi <= 2899999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else if (mmsi >= 2900000000L && mmsi <= 2999999999L) {
return "OTHER"; // Другие типы судов (дополнительный диапазон)
} else {
return "UNKNOWN"; // Неизвестный тип
}
}
/**
* Кодирует цвет для безопасного использования в URL
* Специально обрабатывает HEX цвета, заменяя # на %23
* @param color Цвет в формате HEX (#RRGGBB) или имя цвета
* @return Закодированный цвет
*/
private static String encodeColorForURL(String color) {
if (color == null || color.trim().isEmpty()) {
return "green"; // Цвет по умолчанию
}
try {
// Если цвет начинается с #, заменяем его на %23
if (color.startsWith("#")) {
String encoded = "%23" + color.substring(1);
Log.d(TAG, "Закодирован HEX цвет: " + color + " -> " + encoded);
return encoded;
} else {
// Для именованных цветов используем стандартное кодирование
String encoded = URLEncoder.encode(color, StandardCharsets.UTF_8.toString());
Log.d(TAG, "Закодирован именованный цвет: " + color + " -> " + encoded);
return encoded;
}
} catch (Exception e) {
Log.e(TAG, "Ошибка кодирования цвета: " + e.getMessage(), e);
return "green"; // Цвет по умолчанию
}
}
/** /**
* Кодирует строку для безопасного использования в URL * Кодирует строку для безопасного использования в URL
* Дополнительно экранирует символы, которые могут вызывать проблемы * Дополнительно экранирует символы, которые могут вызывать проблемы
@@ -109,12 +307,13 @@ public class LogSender {
String encoded = URLEncoder.encode(message, StandardCharsets.UTF_8.toString()); String encoded = URLEncoder.encode(message, StandardCharsets.UTF_8.toString());
// Дополнительно экранируем символы, которые могут вызывать проблемы // Дополнительно экранируем символы, которые могут вызывать проблемы
// Заменяем < на %3C, > на %3E, & на %26, " на %22, ' на %27 // Заменяем < на %3C, > на %3E, & на %26, " на %22, ' на %27, # на %23
encoded = encoded.replace("<", "%3C") encoded = encoded.replace("<", "%3C")
.replace(">", "%3E") .replace(">", "%3E")
.replace("&", "%26") .replace("&", "%26")
.replace("\"", "%22") .replace("\"", "%22")
.replace("'", "%27"); .replace("'", "%27")
.replace("#", "%23");
// Логируем для отладки // Логируем для отладки
Log.d(TAG, "Исходное сообщение: " + message); Log.d(TAG, "Исходное сообщение: " + message);
@@ -129,6 +328,7 @@ public class LogSender {
.replace("&", "%26") .replace("&", "%26")
.replace("\"", "%22") .replace("\"", "%22")
.replace("'", "%27") .replace("'", "%27")
.replace("#", "%23")
.replace(" ", "%20"); .replace(" ", "%20");
Log.d(TAG, "Fallback кодирование: " + fallback); Log.d(TAG, "Fallback кодирование: " + fallback);
return fallback; return fallback;