generated from Grigo/AndroidTemplate
Подготовка к крупным изменениям: карта, 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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user