From bdc0aa3ccfc460b4128011f41004031085203acb Mon Sep 17 00:00:00 2001 From: grigo Date: Tue, 9 Sep 2025 14:24:12 +0300 Subject: [PATCH] fixed NMEAParser --- .../aismap/controllers/NMEAParser.java | 371 ++++++++++++++---- .../aismap/maps/YandexMarkerWrapper.java | 2 +- .../grigowashere/aismap/utils/LogSender.java | 208 +++++++++- 3 files changed, 508 insertions(+), 73 deletions(-) diff --git a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java index 7bf49bb..8d6a2a2 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java @@ -11,6 +11,11 @@ import java.util.ArrayList; /** * Контроллер для парсинга 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); int messageType = Integer.parseInt(messageTypeBits, 2); - Log.d(TAG, "Декодируем AIS тип " + messageType + " на канале " + channel); + 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: @@ -892,7 +905,12 @@ public class NMEAParser { // Вырезаем нужный диапазон битов 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 { Log.w(TAG, "AIS поле выходит за границы: startBit=" + startBit + @@ -1043,11 +1061,11 @@ public class NMEAParser { int vesselTypeCode = Integer.parseInt(typeBits, 2); Log.d(TAG, "Type bits: " + typeBits + " = " + vesselTypeCode); - // Dimension Reference (4 бита) - бит 240 - String dimRefABits = decodeAISField(payload, 240, 4); - String dimRefBBits = decodeAISField(payload, 244, 4); - String dimRefCBits = decodeAISField(payload, 248, 4); - String dimRefDBits = decodeAISField(payload, 252, 4); + // 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); @@ -1056,18 +1074,25 @@ public class NMEAParser { Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD); - // Vessel Dimensions (30 бит) - бит 256 - String lengthBits = decodeAISField(payload, 256, 10); - String widthBits = decodeAISField(payload, 266, 10); - String draftBits = decodeAISField(payload, 276, 8); + // Для сообщения типа 5 используем Dimension Reference поля (9, 9, 6, 6 бит) + // Размеры судна рассчитываются как: + // Длина = Dim.A + Dim.B (от носа до антенны + от антенны до кормы) + // Ширина = Dim.C + Dim.D (от левого борта до антенны + от антенны до правого борта) + double length = dimRefA + dimRefB; + double width = dimRefC + dimRefD; - double length = Integer.parseInt(lengthBits, 2); - double width = Integer.parseInt(widthBits, 2); + // Draft (8 бит) - осадка - бит 296 + String draftBits = decodeAISField(payload, 296, 8); double draft = Integer.parseInt(draftBits, 2) / 10.0; - Log.d(TAG, "Dimensions - Length bits: " + lengthBits + " = " + length); - Log.d(TAG, "Dimensions - Width bits: " + widthBits + " = " + width); - Log.d(TAG, "Dimensions - Draft bits: " + draftBits + " = " + draft); + 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 бит) - бит 294 String etaBits = decodeAISField(payload, 294, 20); @@ -1374,31 +1399,112 @@ public class NMEAParser { } /** - * Получает тип судна по коду + * Получает тип судна по коду согласно стандарту AIS */ private String getVesselType(int typeCode) { - if (typeCode >= 20 && typeCode <= 29) return "Wing in ground"; - if (typeCode >= 30 && typeCode <= 39) return "Fishing"; - if (typeCode >= 40 && typeCode <= 49) return "Towing"; - if (typeCode >= 50 && typeCode <= 59) return "Dredging"; - if (typeCode >= 60 && typeCode <= 69) return "Diving"; - if (typeCode >= 70 && typeCode <= 79) return "Military"; - if (typeCode >= 80 && typeCode <= 89) return "Pleasure"; - if (typeCode >= 90 && typeCode <= 99) return "High speed"; - if (typeCode >= 100 && typeCode <= 109) return "Pilot vessel"; - if (typeCode >= 110 && typeCode <= 119) return "SAR"; - if (typeCode >= 120 && typeCode <= 129) return "Tug"; - if (typeCode >= 130 && typeCode <= 139) return "Port tender"; - if (typeCode >= 140 && typeCode <= 149) return "Anti-pollution"; - if (typeCode >= 150 && typeCode <= 159) return "Law enforce"; - if (typeCode >= 160 && typeCode <= 169) return "Spare"; - if (typeCode >= 170 && typeCode <= 179) return "Medical"; - if (typeCode >= 180 && typeCode <= 189) return "Special craft"; - if (typeCode >= 190 && typeCode <= 199) return "Passenger"; - if (typeCode >= 200 && typeCode <= 209) return "Cargo"; - if (typeCode >= 210 && typeCode <= 219) return "Tanker"; - if (typeCode >= 220 && typeCode <= 229) return "Other"; - return "Unknown"; + 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"; + } } /** @@ -1767,6 +1873,9 @@ public class NMEAParser { 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"); @@ -1789,6 +1898,15 @@ public class NMEAParser { 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); @@ -1855,17 +1973,86 @@ public class NMEAParser { int dimRefC = Integer.parseInt(dimRefCBits, 2); int dimRefD = Integer.parseInt(dimRefDBits, 2); - // Vessel Dimensions (30 бит) - бит 287 - String lengthBits = decodeAISField(payload, 287, 10); - String widthBits = decodeAISField(payload, 297, 10); - String draftBits = decodeAISField(payload, 307, 8); + Log.d(TAG, "Dimension Reference: A=" + dimRefA + ", B=" + dimRefB + ", C=" + dimRefC + ", D=" + dimRefD); - double length = Integer.parseInt(lengthBits, 2); - double width = Integer.parseInt(widthBits, 2); - double draft = Integer.parseInt(draftBits, 2) / 10.0; + // 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; + } - 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", - mmsi, vesselName, latitude, longitude, course, speed, vesselTypeCode, length, width, draft)); + // 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)); @@ -1876,13 +2063,12 @@ public class NMEAParser { vessel.setVesselType(getVesselType(vesselTypeCode)); vessel.setLength(length); vessel.setWidth(width); - vessel.setDraft(draft); 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, D=%.1f", - vesselName, latitude, longitude, course, speed, getVesselType(vesselTypeCode), length, width, draft); + 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); // Уведомляем слушателя @@ -1944,12 +2130,27 @@ public class NMEAParser { int dimRefD = Integer.parseInt(dimRefDBits, 2); // Vessel Dimensions (30 бит) - бит 235 - String lengthBits = decodeAISField(payload, 235, 10); - String widthBits = decodeAISField(payload, 245, 10); - String draftBits = decodeAISField(payload, 255, 8); + // 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); - double length = Integer.parseInt(lengthBits, 2); - double width = Integer.parseInt(widthBits, 2); + 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", @@ -2027,25 +2228,59 @@ public class NMEAParser { String callSign = decodeAISString(callSignBits); Log.d(TAG, "Call Sign bits: " + callSignBits + " = '" + callSign + "'"); - // Dimension Reference (4 бита) - бит 132 - String dimRefABits = decodeAISField(payload, 132, 4); - String dimRefBBits = decodeAISField(payload, 136, 4); - String dimRefCBits = decodeAISField(payload, 140, 4); - String dimRefDBits = decodeAISField(payload, 144, 4); + // 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); - // Vessel Dimensions (30 бит) - бит 148 - String lengthBits = decodeAISField(payload, 148, 10); - String widthBits = decodeAISField(payload, 158, 10); - String draftBits = decodeAISField(payload, 168, 8); + 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); - double length = Integer.parseInt(lengthBits, 2); - double width = Integer.parseInt(widthBits, 2); - double draft = Integer.parseInt(draftBits, 2) / 10.0; + // Проверяем, есть ли достаточно битов для размеров + 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)); diff --git a/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java index 9fde5ff..f4ec683 100644 --- a/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java @@ -92,7 +92,7 @@ public class YandexMarkerWrapper extends MarkerWrapper { private void createMarker() { try { - double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLongitude(); + double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLatitude(); double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude(); // Сначала создаем иконку diff --git a/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java b/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java index bdab5f7..4ae1d40 100644 --- a/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java +++ b/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java @@ -58,11 +58,55 @@ public class LogSender { message += " | " + vesselInfo; } + // Извлекаем тип судна из vesselInfo и генерируем цвет + // Генерируем уникальный цвет для корабля на основе MMSI + String vesselColor = generateVesselColor(mmsi); + 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); - 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) { 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 * Дополнительно экранирует символы, которые могут вызывать проблемы @@ -109,12 +307,13 @@ public class LogSender { String encoded = URLEncoder.encode(message, StandardCharsets.UTF_8.toString()); // Дополнительно экранируем символы, которые могут вызывать проблемы - // Заменяем < на %3C, > на %3E, & на %26, " на %22, ' на %27 + // Заменяем < на %3C, > на %3E, & на %26, " на %22, ' на %27, # на %23 encoded = encoded.replace("<", "%3C") .replace(">", "%3E") .replace("&", "%26") .replace("\"", "%22") - .replace("'", "%27"); + .replace("'", "%27") + .replace("#", "%23"); // Логируем для отладки Log.d(TAG, "Исходное сообщение: " + message); @@ -129,6 +328,7 @@ public class LogSender { .replace("&", "%26") .replace("\"", "%22") .replace("'", "%27") + .replace("#", "%23") .replace(" ", "%20"); Log.d(TAG, "Fallback кодирование: " + fallback); return fallback;