Подготовка к крупным изменениям: карта, AIS и UI

- Яндекс/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 (не влияют на сборку)
This commit is contained in:
2025-09-23 11:53:23 +03:00
parent a2f1775f9f
commit 41432665ea
37 changed files with 6561 additions and 161 deletions
@@ -952,8 +952,26 @@ public class NMEAParser {
// Rate of Turn (8 бит) - бит 42
String rotBits = decodeAISField(payload, 42, 8);
int rot = Integer.parseInt(rotBits, 2);
Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rot);
int rotRaw = Integer.parseInt(rotBits, 2);
if (rotRaw > 127) {
rotRaw -= 256;
}
double rateOfTurn = parseRateOfTurn(rotRaw);
Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rotRaw + " -> " + rateOfTurn + " °/min");
// Дополнительная отладка - показываем все биты payload
String fullBinary = payloadToBinary(payload);
Log.d(TAG, "Full payload binary: " + fullBinary);
Log.d(TAG, "ROT bits 42-49: " + fullBinary.substring(42, Math.min(50, fullBinary.length())));
// Ищем ROT в разных позициях для отладки
// for (int pos = 0; pos < Math.min(fullBinary.length() - 8, 100); pos++) {
// String testBits = fullBinary.substring(pos, pos + 8);
// int testValue = Integer.parseInt(testBits, 2);
// double testRot = parseRateOfTurn(testValue);
// Log.d(TAG, String.format("Position %d: bits=%s, value=%d, rot=%.1f",
// pos, testBits, testValue, testRot));
// }
// Speed Over Ground (10 бит) - бит 50
String speedBits = decodeAISField(payload, 50, 10);
@@ -998,20 +1016,44 @@ public class NMEAParser {
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",
mmsi, latitude, longitude, course, speed, status, heading));
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);
vessel.updatePosition(latitude, longitude, course, speed, rateOfTurn);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setHeading(heading);
vessel.setNavigationalStatus(getNavigationStatus(status));
vessel.setLastUpdate(java.time.LocalDateTime.now());
// Помечаем класс судна как Class A, чтобы предотвратить дальнейшее перезаписывание Class B сообщениями
vessel.setVesselClass("Class A");
// Отправляем информацию о корабле на внешний ресурс
String vesselInfo = String.format("lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%s",
latitude, longitude, course, speed, getNavigationStatus(status));
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
// Отправляем информацию о корабле на внешний ресурс (помечаем как Class A и добавляем статические поля, если известны)
StringBuilder infoA = new StringBuilder(
String.format(java.util.Locale.US,
"Class A: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, rot=%.1f, status=%s",
latitude, longitude, course, speed, rateOfTurn, getNavigationStatus(status))
);
if (vessel.getVesselName() != null && !vessel.getVesselName().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", name='%s'", vessel.getVesselName()));
}
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
}
if (vessel.getVesselType() != null && !vessel.getVesselType().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", type=%s", vessel.getVesselType()));
}
if (vessel.getLength() > 0 || vessel.getWidth() > 0) {
infoA.append(String.format(java.util.Locale.US, ", L=%.1f, W=%.1f", vessel.getLength(), vessel.getWidth()));
}
if (vessel.getDraft() > 0) {
infoA.append(String.format(java.util.Locale.US, ", D=%.1f", vessel.getDraft()));
}
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
infoA.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
}
LogSender.logShipUpdate(String.valueOf(mmsi), infoA.toString());
// Уведомляем слушателя
if (listener != null) {
@@ -1147,8 +1189,9 @@ public class NMEAParser {
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'",
// Отправляем информацию о корабле на внешний ресурс (помечаем как Class A Static)
String vesselInfo = String.format(java.util.Locale.US,
"Class A Static: 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);
@@ -1207,6 +1250,48 @@ public class NMEAParser {
}
}
/**
* Преобразует AIS payload в полную битовую строку для отладки
*/
private String payloadToBinary(String payload) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < payload.length(); i++) {
int ascii = payload.charAt(i);
int value;
if (ascii >= 48 && ascii <= 87) {
value = ascii - 48;
} else if (ascii >= 88 && ascii <= 119) {
value = ascii - 56;
} else {
value = 0;
}
String binary = String.format("%6s", Integer.toBinaryString(value)).replace(' ', '0');
result.append(binary);
}
return result.toString();
}
/**
* Парсит Rate of Turn согласно стандарту AIS
* ROTAIS = 4.733 SQRT(ROTINDICATED) degrees/min
* Значения: 0-126 = поворот вправо, 127 = поворот влево >5°/30с, 128-255 = поворот влево
*/
private double parseRateOfTurn(int rotRaw) {
if (rotRaw == -128) {
return Double.NaN; // Нет данных
}
if (rotRaw == -127) {
return -720.0; // Влево > 708°/мин
}
if (rotRaw == 127) {
return 720.0; // Вправо > 708°/мин
}
// В диапазоне -126..126
double rot = rotRaw / 4.733;
return Math.signum(rotRaw) * rot * rot;
}
/**
* Парсит AIS координаты
*/
@@ -1235,31 +1320,95 @@ public class NMEAParser {
}
/**
* Декодирует AIS строку
* Декодирует AIS строку согласно стандарту ITU-R M.1371-5, таблица 44
* Простой switch case для всех 64 возможных значений 6-битной кодировки
*/
//TODO: Исправить на нормальный декодер строк
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;
if (value == 0) {
decodedChar = ' '; // 0 = пробел
} else if (value >= 1 && value <= 26) {
decodedChar = (char) ('A' + value - 1); // 1..26 = A..Z
} else if (value >= 27 && value <= 36) {
decodedChar = (char) ('0' + (value - 27)); // 27..36 = 0..9
} else {
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);
}
return result.toString().trim();
String resultStr = result.toString().trim();
Log.d(TAG, "Результат декодирования: '" + resultStr + "'");
return resultStr;
}
/**
@@ -1703,6 +1852,12 @@ public class NMEAParser {
Log.d(TAG, "Safety Text bits: " + textBits + " = '" + safetyText + "'");
Log.d(TAG, String.format("AIS Safety Broadcast: MMSI=%d, text='%s'", mmsi, safetyText));
// Отправляем лог наружу
try {
com.grigowashere.aismap.utils.LogSender.logShipUpdate(String.valueOf(mmsi), "Safety: " + safetyText);
} catch (Throwable t) {
Log.w(TAG, "Ошибка отправки safety-лога: " + t.getMessage());
}
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
@@ -1781,19 +1936,52 @@ public class NMEAParser {
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
// Логика приоритета классов:
// - Если уже Class A: игнорируем обновление типа 18 полностью
// - Если Extended Class B: обновляем только динамику (позиция, скорость, курс и т.п.), класс не меняем
String existingClass = vessel.getVesselClass();
if ("Class A".equals(existingClass)) {
Log.d(TAG, "Пропускаем обновление Class B (тип 18) для судна класса Class A: " + mmsi);
return;
}
boolean keepExtended = "Extended Class B".equals(existingClass);
vessel.updatePosition(latitude, longitude, course, speed);
vessel.setHeading(heading);
vessel.setPositionAccuracy(accuracy == 1);
vessel.setLastUpdate(java.time.LocalDateTime.now());
vessel.setVesselClass("Class B");
if (!keepExtended) {
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);
// Добавляем статические поля, если они уже известны (из сообщений 24 и др.)
StringBuilder info = new StringBuilder(
String.format(java.util.Locale.US,
"Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
latitude, longitude, course, speed, heading, (accuracy == 1 ? "high" : "low"))
);
if (vessel.getVesselName() != null && !vessel.getVesselName().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", name='%s'", vessel.getVesselName()));
}
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
}
if (vessel.getVesselType() != null && !vessel.getVesselType().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", type=%s", vessel.getVesselType()));
}
if (vessel.getLength() > 0 || vessel.getWidth() > 0) {
info.append(String.format(java.util.Locale.US, ", L=%.1f, W=%.1f", vessel.getLength(), vessel.getWidth()));
}
if (vessel.getDraft() > 0) {
info.append(String.format(java.util.Locale.US, ", D=%.1f", vessel.getDraft()));
}
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
info.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
}
LogSender.logShipUpdate(String.valueOf(mmsi), info.toString());
// Уведомляем слушателя
if (listener != null) {
@@ -1895,6 +2083,12 @@ public class NMEAParser {
Log.w(TAG, "Extended Class B - недостаточно битов для размеров: " + totalBits + " < 327");
// Создаем судно без размеров
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
// Если судно уже Class A, не перезаписываем данными Extended Class B
String existingClassShort = vessel.getVesselClass();
if ("Class A".equals(existingClassShort)) {
Log.d(TAG, "Пропускаем обновление Extended Class B для судна класса Class A: " + mmsi);
return;
}
vessel.updatePosition(latitude, longitude, course, speed);
vessel.setHeading(heading);
vessel.setPositionAccuracy(accuracy == 1);
@@ -1906,6 +2100,19 @@ public class NMEAParser {
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
// Логируем короткое сообщение типа 19 с доступными данными
StringBuilder shortInfo = new StringBuilder(
String.format(java.util.Locale.US,
"Extended Class B (short): name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
vesselName, latitude, longitude, course, speed, heading, (accuracy == 1 ? "high" : "low"))
);
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
shortInfo.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
}
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
shortInfo.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
}
LogSender.logShipUpdate(String.valueOf(mmsi), shortInfo.toString());
return;
}
@@ -1970,6 +2177,12 @@ public class NMEAParser {
// Создаем или обновляем AIS судно
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
// Если судно уже Class A, не перезаписываем данными Extended Class B
String existingClassFull = vessel.getVesselClass();
if ("Class A".equals(existingClassFull)) {
Log.d(TAG, "Пропускаем обновление Extended Class B для судна класса Class A: " + mmsi);
return;
}
vessel.updatePosition(latitude, longitude, course, speed);
vessel.setHeading(heading);
vessel.setPositionAccuracy(accuracy == 1);
@@ -2125,6 +2338,12 @@ public class NMEAParser {
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
// Логируем статические данные Class B (Part A)
String vesselInfo = String.format(java.util.Locale.US,
"Class B Static A: name='%s'",
vesselName);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
} else if (partNumber == 1) {
// Part B: Vessel Type, Dimensions, etc.
@@ -2212,6 +2431,15 @@ public class NMEAParser {
if (listener != null) {
listener.onAISVesselUpdated(vessel);
}
// Логируем статические данные Class B (Part B)
String vesselInfoB = String.format(java.util.Locale.US,
"Class B Static B: name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f",
vessel.getVesselName() != null ? vessel.getVesselName() : "",
callSign,
getVesselType(vesselTypeCode),
length, width, draft);
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfoB);
}
} catch (Exception e) {