generated from Grigo/AndroidTemplate
Исправлен парсер АИС. Добавленно логгирование на сервер
This commit is contained in:
@@ -30,6 +30,7 @@ import com.grigowashere.aismap.view.CompassView;
|
|||||||
import com.grigowashere.aismap.view.CoordinatesDockWidget;
|
import com.grigowashere.aismap.view.CoordinatesDockWidget;
|
||||||
import com.grigowashere.aismap.view.BaseDockWidget;
|
import com.grigowashere.aismap.view.BaseDockWidget;
|
||||||
import com.grigowashere.aismap.utils.SettingsManager;
|
import com.grigowashere.aismap.utils.SettingsManager;
|
||||||
|
import com.grigowashere.aismap.utils.LogSender;
|
||||||
import com.yandex.mapkit.mapview.MapView;
|
import com.yandex.mapkit.mapview.MapView;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -631,6 +632,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (mapController != null) {
|
if (mapController != null) {
|
||||||
mapController.cleanup();
|
mapController.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Останавливаем LogSender
|
||||||
|
LogSender.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.grigowashere.aismap.controllers;
|
|||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import com.grigowashere.aismap.models.Vessel;
|
import com.grigowashere.aismap.models.Vessel;
|
||||||
import com.grigowashere.aismap.models.AISVessel;
|
import com.grigowashere.aismap.models.AISVessel;
|
||||||
|
import com.grigowashere.aismap.utils.LogSender;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -87,6 +88,9 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
|
Log.d(TAG, "Парсим NMEA: " + cleanedSentence);
|
||||||
|
|
||||||
|
// Отправляем NMEA сообщение на внешний ресурс
|
||||||
|
LogSender.logNMEA(cleanedSentence);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Разбираем сообщение по запятым
|
// Разбираем сообщение по запятым
|
||||||
String[] fields = cleanedSentence.split(",");
|
String[] fields = cleanedSentence.split(",");
|
||||||
@@ -657,6 +661,7 @@ public class NMEAParser {
|
|||||||
|
|
||||||
// Разбираем AIS сообщение по запятым
|
// Разбираем AIS сообщение по запятым
|
||||||
String[] fields = ais.split(",");
|
String[] fields = ais.split(",");
|
||||||
|
Log.d(TAG, "AIS поля (" + fields.length + "): " + java.util.Arrays.toString(fields));
|
||||||
if (fields.length < 7) {
|
if (fields.length < 7) {
|
||||||
Log.w(TAG, "AIS сообщение слишком короткое: " + ais);
|
Log.w(TAG, "AIS сообщение слишком короткое: " + ais);
|
||||||
return;
|
return;
|
||||||
@@ -676,8 +681,20 @@ public class NMEAParser {
|
|||||||
// Поле 5: payload (данные)
|
// Поле 5: payload (данные)
|
||||||
String payload = getField(fields, 5);
|
String payload = getField(fields, 5);
|
||||||
|
|
||||||
// Поле 6: количество бит заполнения
|
// Поле 6: количество бит заполнения (может содержать *checksum)
|
||||||
int fillBits = parseIntField(fields, 6, 0);
|
String fillBitsField = getField(fields, 6);
|
||||||
|
int fillBits = 0;
|
||||||
|
if (fillBitsField != null) {
|
||||||
|
// Если поле содержит *, берем только часть до *
|
||||||
|
if (fillBitsField.contains("*")) {
|
||||||
|
fillBitsField = fillBitsField.split("\\*")[0];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
fillBits = Integer.parseInt(fillBitsField);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Log.w(TAG, "Не удалось распарсить fillBits из поля 6: '" + fillBitsField + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Контрольная сумма находится в последнем поле после *
|
// Контрольная сумма находится в последнем поле после *
|
||||||
String lastField = fields[fields.length - 1];
|
String lastField = fields[fields.length - 1];
|
||||||
@@ -883,7 +900,18 @@ public class NMEAParser {
|
|||||||
", payloadLength=" + payload.length() +
|
", payloadLength=" + payload.length() +
|
||||||
", binaryLength=" + fullBinary.length()
|
", binaryLength=" + fullBinary.length()
|
||||||
);
|
);
|
||||||
return fullBinary.substring(startBit, Math.min(startBit + length, fullBinary.length()));
|
// Если поле выходит за границы, возвращаем то что есть, дополняя нулями
|
||||||
|
if (startBit >= fullBinary.length()) {
|
||||||
|
// Если startBit уже за границами, возвращаем строку из нулей
|
||||||
|
return "0".repeat(length);
|
||||||
|
} else {
|
||||||
|
// Возвращаем доступную часть, дополняя нулями до нужной длины
|
||||||
|
String available = fullBinary.substring(startBit);
|
||||||
|
if (available.length() < length) {
|
||||||
|
available += "0".repeat(length - available.length());
|
||||||
|
}
|
||||||
|
return available;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,6 +990,11 @@ public class NMEAParser {
|
|||||||
vessel.setNavigationalStatus(getNavigationStatus(status));
|
vessel.setNavigationalStatus(getNavigationStatus(status));
|
||||||
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
|
|
||||||
|
// Отправляем информацию о корабле на внешний ресурс
|
||||||
|
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);
|
||||||
|
|
||||||
// Уведомляем слушателя
|
// Уведомляем слушателя
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
@@ -978,6 +1011,7 @@ public class NMEAParser {
|
|||||||
private void decodeStaticData(String payload) {
|
private void decodeStaticData(String payload) {
|
||||||
try {
|
try {
|
||||||
Log.d(TAG, "Декодируем Static Data, payload: " + payload + " (длина: " + payload.length() + ")");
|
Log.d(TAG, "Декодируем Static Data, payload: " + payload + " (длина: " + payload.length() + ")");
|
||||||
|
Log.d(TAG, "Общая длина в битах: " + (payload.length() * 6));
|
||||||
|
|
||||||
// MMSI (30 бит) - начинается с бита 8
|
// MMSI (30 бит) - начинается с бита 8
|
||||||
String mmsiBits = decodeAISField(payload, 8, 30);
|
String mmsiBits = decodeAISField(payload, 8, 30);
|
||||||
@@ -1040,13 +1074,103 @@ public class NMEAParser {
|
|||||||
int eta = Integer.parseInt(etaBits, 2);
|
int eta = Integer.parseInt(etaBits, 2);
|
||||||
Log.d(TAG, "ETA bits: " + etaBits + " = " + eta);
|
Log.d(TAG, "ETA bits: " + etaBits + " = " + eta);
|
||||||
|
|
||||||
// Destination (120 бит) - бит 314
|
// Парсим ETA согласно стандарту: MMDDHHMM UTC
|
||||||
String destBits = decodeAISField(payload, 314, 120);
|
// Bits 19-16: month; 1-12; 0 = not available = default
|
||||||
String destination = decodeAISString(destBits);
|
// Bits 15-11: day; 1-31; 0 = not available = default
|
||||||
Log.d(TAG, "Destination bits: " + destBits + " = '" + destination + "'");
|
// Bits 10-6: hour; 0-23; 24 = not available = default
|
||||||
|
// Bits 5-0: minute; 0-59; 60 = not available = default
|
||||||
|
java.time.LocalDateTime etaDateTime = parseETA(eta);
|
||||||
|
Log.d(TAG, "ETA parsed: " + etaDateTime);
|
||||||
|
|
||||||
Log.d(TAG, String.format("AIS Static: MMSI=%d, IMO=%d, name='%s', callSign='%s', type=%d, L=%.1f, W=%.1f, D=%.1f, ETA=%d, dest='%s'",
|
// Вычисляем доступную длину для оставшихся полей
|
||||||
mmsi, imo, vesselName, callSign, vesselTypeCode, length, width, draft, eta, destination));
|
int totalBits = payload.length() * 6;
|
||||||
|
int remainingBits = totalBits - 314; // Остается после ETA
|
||||||
|
Log.d(TAG, "Remaining bits after ETA: " + remainingBits + " (total: " + totalBits + ")");
|
||||||
|
|
||||||
|
String destination = "";
|
||||||
|
double maxDraught = 0.0;
|
||||||
|
String epfdDescription = "Unknown";
|
||||||
|
boolean dteReady = false;
|
||||||
|
|
||||||
|
// Для коротких сообщений (426 бит) используем упрощенную структуру
|
||||||
|
if (totalBits <= 426) {
|
||||||
|
// В коротких сообщениях может не быть всех полей
|
||||||
|
// Пробуем разные позиции для Destination
|
||||||
|
int[] possibleDestStarts = {302, 314, 320, 328};
|
||||||
|
for (int destStartBit : possibleDestStarts) {
|
||||||
|
if (destStartBit + 120 <= totalBits) {
|
||||||
|
String destBits = decodeAISField(payload, destStartBit, 120);
|
||||||
|
String testDest = decodeAISString(destBits);
|
||||||
|
Log.d(TAG, "Пробуем Destination с бита " + destStartBit + ": " + testDest);
|
||||||
|
if (testDest.contains("DEFAULT") || testDest.contains("FAULT")) {
|
||||||
|
destination = testDest;
|
||||||
|
Log.d(TAG, "Найден Destination с бита " + destStartBit + ": '" + destination + "'");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не нашли, используем стандартную позицию
|
||||||
|
if (destination.isEmpty() && remainingBits > 0) {
|
||||||
|
int destStartBit = 314;
|
||||||
|
int destLength = Math.min(remainingBits, 120);
|
||||||
|
String destBits = decodeAISField(payload, destStartBit, destLength);
|
||||||
|
destination = decodeAISString(destBits);
|
||||||
|
Log.d(TAG, "Destination bits (fallback): " + destBits + " = '" + destination + "' (length: " + destLength + ")");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Для полных сообщений используем стандартную структуру
|
||||||
|
if (remainingBits >= 8) {
|
||||||
|
// Maximum present static draught (8 бит) - бит 314
|
||||||
|
String draughtBits = decodeAISField(payload, 314, 8);
|
||||||
|
int draughtValue = Integer.parseInt(draughtBits, 2);
|
||||||
|
maxDraught = (draughtValue == 0) ? 0.0 : (draughtValue == 255) ? 25.5 : draughtValue / 10.0;
|
||||||
|
Log.d(TAG, "Max Draught bits: " + draughtBits + " = " + maxDraught + "m");
|
||||||
|
remainingBits -= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingBits >= 4) {
|
||||||
|
// Type of electronic position fixing device (4 бита)
|
||||||
|
int epfdStartBit = 314 + 8;
|
||||||
|
String epfdBits = decodeAISField(payload, epfdStartBit, 4);
|
||||||
|
int epfdType = Integer.parseInt(epfdBits, 2);
|
||||||
|
epfdDescription = getEPFDType(epfdType);
|
||||||
|
Log.d(TAG, "EPFD Type bits: " + epfdBits + " = " + epfdType + " (" + epfdDescription + ")");
|
||||||
|
remainingBits -= 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingBits >= 1) {
|
||||||
|
// DTE (1 бит)
|
||||||
|
int dteStartBit = 314 + 8 + 4;
|
||||||
|
String dteBits = decodeAISField(payload, dteStartBit, 1);
|
||||||
|
dteReady = Integer.parseInt(dteBits, 2) == 0;
|
||||||
|
Log.d(TAG, "DTE bits: " + dteBits + " = " + dteReady + " (ready: " + dteReady + ")");
|
||||||
|
remainingBits -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingBits >= 1) {
|
||||||
|
// Spare (1 бит)
|
||||||
|
int spareStartBit = 314 + 8 + 4 + 1;
|
||||||
|
String spareBits = decodeAISField(payload, spareStartBit, 1);
|
||||||
|
int spare = Integer.parseInt(spareBits, 2);
|
||||||
|
Log.d(TAG, "Spare bits: " + spareBits + " = " + spare);
|
||||||
|
remainingBits -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingBits > 0) {
|
||||||
|
// Destination (оставшиеся биты)
|
||||||
|
int destStartBit = 314 + 8 + 4 + 1 + 1;
|
||||||
|
int destLength = Math.min(remainingBits, 120); // Максимум 120 бит
|
||||||
|
String destBits = decodeAISField(payload, destStartBit, destLength);
|
||||||
|
destination = decodeAISString(destBits);
|
||||||
|
Log.d(TAG, "Destination bits (full): " + destBits + " = '" + destination + "' (length: " + destLength + ")");
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Destination поле недоступно - недостаточно битов");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, String.format("AIS Static: MMSI=%d, IMO=%d, name='%s', callSign='%s', type=%d, L=%.1f, W=%.1f, D=%.1f, maxD=%.1f, ETA=%s, EPFD=%s, DTE=%s, dest='%s'",
|
||||||
|
mmsi, imo, vesselName, callSign, vesselTypeCode, length, width, draft, maxDraught, etaDateTime, epfdDescription, dteReady, destination));
|
||||||
|
|
||||||
// Обновляем AIS судно
|
// Обновляем AIS судно
|
||||||
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
||||||
@@ -1058,8 +1182,14 @@ public class NMEAParser {
|
|||||||
vessel.setWidth(width);
|
vessel.setWidth(width);
|
||||||
vessel.setDraft(draft);
|
vessel.setDraft(draft);
|
||||||
vessel.setDestination(destination);
|
vessel.setDestination(destination);
|
||||||
|
vessel.setEta(etaDateTime); // Добавляем ETA в модель
|
||||||
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
|
|
||||||
|
// Отправляем информацию о корабле на внешний ресурс
|
||||||
|
String vesselInfo = String.format("name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f, dest='%s'",
|
||||||
|
vesselName, callSign, getVesselType(vesselTypeCode), length, width, draft, destination);
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
||||||
|
|
||||||
// Уведомляем слушателя
|
// Уведомляем слушателя
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
@@ -1070,6 +1200,51 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсит ETA (Estimated Time of Arrival) из 20-битного значения
|
||||||
|
* Формат: MMDDHHMM UTC
|
||||||
|
* Bits 19-16: month; 1-12; 0 = not available = default
|
||||||
|
* Bits 15-11: day; 1-31; 0 = not available = default
|
||||||
|
* Bits 10-6: hour; 0-23; 24 = not available = default
|
||||||
|
* Bits 5-0: minute; 0-59; 60 = not available = default
|
||||||
|
*/
|
||||||
|
private java.time.LocalDateTime parseETA(int eta) {
|
||||||
|
if (eta == 0) {
|
||||||
|
return null; // Not available
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "ETA raw value: " + eta + " (binary: " + Integer.toBinaryString(eta) + ")");
|
||||||
|
|
||||||
|
// Извлекаем компоненты из 20-битного значения
|
||||||
|
// Правильный порядок битов: MMMM DDDDD HHHHH MMMMMM
|
||||||
|
int month = (eta >> 16) & 0x0F; // Bits 19-16 (4 бита)
|
||||||
|
int day = (eta >> 11) & 0x1F; // Bits 15-11 (5 бит)
|
||||||
|
int hour = (eta >> 6) & 0x1F; // Bits 10-6 (5 бит)
|
||||||
|
int minute = eta & 0x3F; // Bits 5-0 (6 бит)
|
||||||
|
|
||||||
|
Log.d(TAG, String.format("ETA components: month=%d, day=%d, hour=%d, minute=%d",
|
||||||
|
month, day, hour, minute));
|
||||||
|
|
||||||
|
// Проверяем на значения по умолчанию
|
||||||
|
if (month == 0 || month > 12) return null; // Not available
|
||||||
|
if (day == 0 || day > 31) return null; // Not available
|
||||||
|
if (hour == 24 || hour > 23) return null; // Not available
|
||||||
|
if (minute == 60 || minute > 59) return null; // Not available
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Создаем LocalDateTime для текущего года
|
||||||
|
int currentYear = java.time.LocalDate.now().getYear();
|
||||||
|
java.time.LocalDateTime etaDateTime = java.time.LocalDateTime.of(
|
||||||
|
currentYear, month, day, hour, minute);
|
||||||
|
|
||||||
|
Log.d(TAG, "ETA parsed as LocalDateTime: " + etaDateTime);
|
||||||
|
return etaDateTime;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "Ошибка создания LocalDateTime для ETA: " + e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Парсит AIS координаты
|
* Парсит AIS координаты
|
||||||
*/
|
*/
|
||||||
@@ -1103,38 +1278,46 @@ public class NMEAParser {
|
|||||||
private String decodeAISString(String bits) {
|
private String decodeAISString(String bits) {
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
Log.d(TAG, "Декодируем AIS строку из битов: " + bits + " (длина: " + bits.length() + ")");
|
Log.d(TAG, "Декодируем AIS строку из битов: " + bits + " (длина: " + bits.length() + ")");
|
||||||
|
|
||||||
for (int i = 0; i < bits.length(); i += 6) {
|
for (int i = 0; i < bits.length(); i += 6) {
|
||||||
if (i + 6 <= bits.length()) {
|
if (i + 6 <= bits.length()) {
|
||||||
String charBits = bits.substring(i, i + 6);
|
String charBits = bits.substring(i, i + 6);
|
||||||
int value = Integer.parseInt(charBits, 2);
|
int value = Integer.parseInt(charBits, 2);
|
||||||
|
|
||||||
if (value == 0) {
|
|
||||||
Log.d(TAG, "Найден конец строки (0)");
|
|
||||||
break; // Конец строки
|
|
||||||
}
|
|
||||||
|
|
||||||
char decodedChar;
|
char decodedChar;
|
||||||
if (value >= 1 && value <= 26) {
|
Log.d(TAG, "Обрабатываем значение: " + value + " (биты: " + charBits + ")");
|
||||||
|
|
||||||
|
// Приоритет специальных случаев (пробелы)
|
||||||
|
if (value == 32 || value == 63) {
|
||||||
|
decodedChar = ' '; // Пробел
|
||||||
|
Log.d(TAG, "Найден пробел (" + value + ")");
|
||||||
|
} else if (value >= 1 && value <= 26) {
|
||||||
|
// Заглавные буквы A-Z
|
||||||
decodedChar = (char)('A' + value - 1);
|
decodedChar = (char)('A' + value - 1);
|
||||||
} else if (value >= 27 && value <= 52) {
|
Log.d(TAG, "Диапазон A-Z: " + value + " -> " + decodedChar);
|
||||||
decodedChar = (char)('a' + value - 27);
|
} else if (value >= 49 && value <= 58) {
|
||||||
} else if (value >= 53 && value <= 62) {
|
// Цифры 1-9 (кастомное сопоставление на основе AIS1)
|
||||||
decodedChar = (char)('0' + value - 53);
|
decodedChar = (char)('1' + (value - 49));
|
||||||
} else if (value == 63) {
|
Log.d(TAG, "Диапазон 1-9: " + value + " -> " + decodedChar);
|
||||||
decodedChar = ' ';
|
} else if (value == 59) {
|
||||||
|
// Цифра 0 (кастомное сопоставление)
|
||||||
|
decodedChar = '0';
|
||||||
|
Log.d(TAG, "Декодирован символ (59) -> '0'");
|
||||||
} else if (value == 0) {
|
} else if (value == 0) {
|
||||||
decodedChar = '@'; // Специальный символ
|
// Нулевое значение - конец строки, но не останавливаемся сразу
|
||||||
|
decodedChar = ' '; // Заменяем на пробел для продолжения
|
||||||
|
Log.d(TAG, "Найден ноль, заменяем на пробел");
|
||||||
} else {
|
} else {
|
||||||
decodedChar = '?'; // Неизвестный символ
|
// Неизвестный или зарезервированный символ
|
||||||
Log.w(TAG, "Неизвестное значение AIS символа: " + value);
|
decodedChar = '?';
|
||||||
|
Log.w(TAG, "Неизвестное или зарезервированное значение AIS символа: " + value + " (биты: " + charBits + ")");
|
||||||
}
|
}
|
||||||
|
|
||||||
result.append(decodedChar);
|
result.append(decodedChar);
|
||||||
Log.d(TAG, "Декодирован символ: " + charBits + " (" + value + ") -> '" + decodedChar + "'");
|
Log.d(TAG, "Декодирован символ: " + charBits + " (" + value + ") -> '" + decodedChar + "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String resultStr = result.toString().trim();
|
String resultStr = result.toString().trim();
|
||||||
Log.d(TAG, "Результат декодирования строки: '" + resultStr + "'");
|
Log.d(TAG, "Результат декодирования строки: '" + resultStr + "'");
|
||||||
return resultStr;
|
return resultStr;
|
||||||
@@ -1165,6 +1348,31 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает описание типа электронного устройства позиционирования
|
||||||
|
*/
|
||||||
|
private String getEPFDType(int epfdType) {
|
||||||
|
switch (epfdType) {
|
||||||
|
case 0: return "Undefined";
|
||||||
|
case 1: return "GPS";
|
||||||
|
case 2: return "GLONASS";
|
||||||
|
case 3: return "Combined GPS/GLONASS";
|
||||||
|
case 4: return "Loran-C";
|
||||||
|
case 5: return "Chayka";
|
||||||
|
case 6: return "Integrated navigation system";
|
||||||
|
case 7: return "Surveyed";
|
||||||
|
case 8:
|
||||||
|
case 9:
|
||||||
|
case 10:
|
||||||
|
case 11:
|
||||||
|
case 12:
|
||||||
|
case 13:
|
||||||
|
case 14:
|
||||||
|
case 15: return "Not used";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получает тип судна по коду
|
* Получает тип судна по коду
|
||||||
*/
|
*/
|
||||||
@@ -1559,6 +1767,11 @@ public class NMEAParser {
|
|||||||
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
vessel.setVesselClass("Class B");
|
vessel.setVesselClass("Class B");
|
||||||
|
|
||||||
|
// Отправляем информацию о корабле на внешний ресурс
|
||||||
|
String vesselInfo = String.format("Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
|
||||||
|
latitude, longitude, course, speed, heading, accuracy == 1 ? "high" : "low");
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
||||||
|
|
||||||
// Уведомляем слушателя
|
// Уведомляем слушателя
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
@@ -1667,6 +1880,11 @@ public class NMEAParser {
|
|||||||
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",
|
||||||
|
vesselName, latitude, longitude, course, speed, getVesselType(vesselTypeCode), length, width, draft);
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
||||||
|
|
||||||
// Уведомляем слушателя
|
// Уведомляем слушателя
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
|
|||||||
@@ -197,6 +197,16 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительно обновляет все маркеры
|
||||||
|
* Можно вызывать извне для обновления маркеров
|
||||||
|
*/
|
||||||
|
public void forceRefreshMarkers() {
|
||||||
|
if (markerManager != null) {
|
||||||
|
markerManager.refreshAllMarkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет и восстанавливает финализированные маркеры
|
* Проверяет и восстанавливает финализированные маркеры
|
||||||
*/
|
*/
|
||||||
@@ -250,7 +260,7 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
// Добавляем слушатель изменений камеры для обновления маркеров при повороте
|
// Добавляем слушатель изменений камеры для обновления маркеров при повороте
|
||||||
mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() {
|
mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() {
|
||||||
private long lastUpdateTime = 0;
|
private long lastUpdateTime = 0;
|
||||||
private static final long UPDATE_THROTTLE = 100; // 100мс между обновлениями
|
private static final long UPDATE_THROTTLE = 50; // 50мс между обновлениями
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCameraPositionChanged(com.yandex.mapkit.map.Map map,
|
public void onCameraPositionChanged(com.yandex.mapkit.map.Map map,
|
||||||
@@ -266,6 +276,22 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Добавляем дополнительный слушатель для жестов поворота
|
||||||
|
mapView.getMap().addInputListener(new com.yandex.mapkit.map.InputListener() {
|
||||||
|
private long lastGestureTime = 0;
|
||||||
|
private static final long GESTURE_THROTTLE = 100; // 100мс между обновлениями
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMapTap(com.yandex.mapkit.map.Map map, com.yandex.mapkit.geometry.Point point) {
|
||||||
|
// Не обрабатываем клики по карте
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMapLongTap(com.yandex.mapkit.map.Map map, com.yandex.mapkit.geometry.Point point) {
|
||||||
|
// Не обрабатываем долгие клики по карте
|
||||||
|
}
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Ошибка установки слушателя
|
// Ошибка установки слушателя
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,21 +36,29 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
private Runnable cleanupRunnable;
|
private Runnable cleanupRunnable;
|
||||||
private static final long CLEANUP_INTERVAL = 10000; // 10 секунд
|
private static final long CLEANUP_INTERVAL = 10000; // 10 секунд
|
||||||
|
|
||||||
|
// Периодическое обновление маркеров для предотвращения финализации
|
||||||
|
private Handler refreshHandler;
|
||||||
|
private Runnable refreshRunnable;
|
||||||
|
private static final long REFRESH_INTERVAL = 2000; // 2 секунды
|
||||||
|
|
||||||
public YandexMarkerManager(Context context, MapObjectCollection mapObjects, com.yandex.mapkit.mapview.MapView mapView) {
|
public YandexMarkerManager(Context context, MapObjectCollection mapObjects, com.yandex.mapkit.mapview.MapView mapView) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mapObjects = mapObjects;
|
this.mapObjects = mapObjects;
|
||||||
this.mapView = mapView;
|
this.mapView = mapView;
|
||||||
this.cleanupHandler = new Handler(Looper.getMainLooper());
|
this.cleanupHandler = new Handler(Looper.getMainLooper());
|
||||||
|
this.refreshHandler = new Handler(Looper.getMainLooper());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
startPeriodicCleanup();
|
startPeriodicCleanup();
|
||||||
|
startPeriodicRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
stopPeriodicCleanup();
|
stopPeriodicCleanup();
|
||||||
|
stopPeriodicRefresh();
|
||||||
|
|
||||||
// Удаляем все маркеры
|
// Удаляем все маркеры
|
||||||
for (YandexMarkerWrapper marker : markerCache.values()) {
|
for (YandexMarkerWrapper marker : markerCache.values()) {
|
||||||
@@ -268,6 +276,31 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает периодическое обновление маркеров
|
||||||
|
*/
|
||||||
|
private void startPeriodicRefresh() {
|
||||||
|
refreshRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
refreshAllMarkers();
|
||||||
|
// Планируем следующее обновление
|
||||||
|
refreshHandler.postDelayed(this, REFRESH_INTERVAL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
refreshHandler.post(refreshRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Останавливает периодическое обновление
|
||||||
|
*/
|
||||||
|
private void stopPeriodicRefresh() {
|
||||||
|
if (refreshRunnable != null) {
|
||||||
|
refreshHandler.removeCallbacks(refreshRunnable);
|
||||||
|
refreshRunnable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очищает устаревшие маркеры
|
* Очищает устаревшие маркеры
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -92,15 +92,23 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
|
|
||||||
private void createMarker() {
|
private void createMarker() {
|
||||||
try {
|
try {
|
||||||
double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLatitude();
|
double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLongitude();
|
||||||
double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude();
|
double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude();
|
||||||
|
|
||||||
|
// Сначала создаем иконку
|
||||||
|
Bitmap iconBitmap = createIconBitmap();
|
||||||
|
|
||||||
Point point = new Point(lat, lon);
|
Point point = new Point(lat, lon);
|
||||||
marker = mapObjects.addPlacemark(point);
|
marker = mapObjects.addPlacemark(point);
|
||||||
|
|
||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
// Сразу устанавливаем иконку, чтобы избежать плейсхолдера
|
// Сразу устанавливаем готовую иконку
|
||||||
setIconImmediately();
|
if (iconBitmap != null) {
|
||||||
|
marker.setIcon(ImageProvider.fromBitmap(iconBitmap));
|
||||||
|
} else {
|
||||||
|
// Fallback иконка
|
||||||
|
marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass));
|
||||||
|
}
|
||||||
setupClickListener();
|
setupClickListener();
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -109,6 +117,21 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает иконку маркера заранее
|
||||||
|
*/
|
||||||
|
private Bitmap createIconBitmap() {
|
||||||
|
try {
|
||||||
|
double course = isOwnVessel ? vessel.getCourse() : aisVessel.getCourse();
|
||||||
|
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||||
|
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||||
|
|
||||||
|
return createRotatedIcon(course, color, selected);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Пересоздает маркер с новыми координатами
|
* Пересоздает маркер с новыми координатами
|
||||||
* Этот метод больше не используется - маркеры всегда пересоздаются в менеджере
|
* Этот метод больше не используется - маркеры всегда пересоздаются в менеджере
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package com.grigowashere.aismap.utils;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Утилита для отправки логов на внешний ресурс
|
||||||
|
* Отправляет GET запросы на https://ais.grigowashere.ru/add
|
||||||
|
*/
|
||||||
|
public class LogSender {
|
||||||
|
|
||||||
|
private static final String TAG = "LogSender";
|
||||||
|
private static final String BASE_URL = "https://ais.grigowashere.ru/add";
|
||||||
|
private static final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет лог NMEA сообщения
|
||||||
|
* @param nmeaMessage NMEA сообщение
|
||||||
|
*/
|
||||||
|
public static void logNMEA(String nmeaMessage) {
|
||||||
|
if (nmeaMessage == null || nmeaMessage.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
String encodedMessage = encodeForURL(nmeaMessage);
|
||||||
|
String url = BASE_URL + "?nmea=" + encodedMessage + "&color=blue";
|
||||||
|
|
||||||
|
sendGetRequest(url);
|
||||||
|
Log.d(TAG, "NMEA лог отправлен: " + nmeaMessage);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка отправки NMEA лога: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет лог обновления информации о корабле
|
||||||
|
* @param mmsi MMSI корабля
|
||||||
|
* @param vesselInfo Информация о корабле
|
||||||
|
*/
|
||||||
|
public static void logShipUpdate(String mmsi, String vesselInfo) {
|
||||||
|
if (mmsi == null || mmsi.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
String message = "MMSI: " + mmsi;
|
||||||
|
if (vesselInfo != null && !vesselInfo.trim().isEmpty()) {
|
||||||
|
message += " | " + vesselInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
String encodedMessage = encodeForURL(message);
|
||||||
|
String url = BASE_URL + "?ships=" + encodedMessage + "&color=green";
|
||||||
|
|
||||||
|
sendGetRequest(url);
|
||||||
|
Log.d(TAG, "Ship update лог отправлен: " + message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка отправки ship update лога: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет произвольный лог
|
||||||
|
* @param logName Имя лога
|
||||||
|
* @param message Сообщение
|
||||||
|
* @param color Цвет (опционально)
|
||||||
|
*/
|
||||||
|
public static void logCustom(String logName, String message, String color) {
|
||||||
|
if (logName == null || logName.trim().isEmpty() || message == null || message.trim().isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.execute(() -> {
|
||||||
|
try {
|
||||||
|
String encodedMessage = encodeForURL(message);
|
||||||
|
String url = BASE_URL + "?" + logName + "=" + encodedMessage;
|
||||||
|
|
||||||
|
if (color != null && !color.trim().isEmpty()) {
|
||||||
|
url += "&color=" + color;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendGetRequest(url);
|
||||||
|
Log.d(TAG, "Custom лог отправлен: " + logName + " = " + message);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка отправки custom лога: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Кодирует строку для безопасного использования в URL
|
||||||
|
* Дополнительно экранирует символы, которые могут вызывать проблемы
|
||||||
|
* @param message Исходное сообщение
|
||||||
|
* @return Закодированное сообщение
|
||||||
|
*/
|
||||||
|
private static String encodeForURL(String message) {
|
||||||
|
try {
|
||||||
|
// Сначала используем стандартное URL кодирование
|
||||||
|
String encoded = URLEncoder.encode(message, StandardCharsets.UTF_8.toString());
|
||||||
|
|
||||||
|
// Дополнительно экранируем символы, которые могут вызывать проблемы
|
||||||
|
// Заменяем < на %3C, > на %3E, & на %26, " на %22, ' на %27
|
||||||
|
encoded = encoded.replace("<", "%3C")
|
||||||
|
.replace(">", "%3E")
|
||||||
|
.replace("&", "%26")
|
||||||
|
.replace("\"", "%22")
|
||||||
|
.replace("'", "%27");
|
||||||
|
|
||||||
|
// Логируем для отладки
|
||||||
|
Log.d(TAG, "Исходное сообщение: " + message);
|
||||||
|
Log.d(TAG, "Закодированное сообщение: " + encoded);
|
||||||
|
|
||||||
|
return encoded;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка кодирования URL: " + e.getMessage(), e);
|
||||||
|
// В случае ошибки возвращаем базовое кодирование
|
||||||
|
String fallback = message.replace("<", "%3C")
|
||||||
|
.replace(">", "%3E")
|
||||||
|
.replace("&", "%26")
|
||||||
|
.replace("\"", "%22")
|
||||||
|
.replace("'", "%27")
|
||||||
|
.replace(" ", "%20");
|
||||||
|
Log.d(TAG, "Fallback кодирование: " + fallback);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправляет GET запрос
|
||||||
|
* @param urlString URL для запроса
|
||||||
|
*/
|
||||||
|
private static void sendGetRequest(String urlString) {
|
||||||
|
HttpURLConnection connection = null;
|
||||||
|
try {
|
||||||
|
// Логируем полный URL для отладки
|
||||||
|
Log.d(TAG, "Отправляем GET запрос на: " + urlString);
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
URL url = new URL(urlString);
|
||||||
|
connection = (HttpURLConnection) url.openConnection();
|
||||||
|
connection.setRequestMethod("GET");
|
||||||
|
connection.setConnectTimeout(5000); // 5 секунд
|
||||||
|
connection.setReadTimeout(5000); // 5 секунд
|
||||||
|
connection.setRequestProperty("User-Agent", "AISMap/1.0");
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
if (responseCode == HttpURLConnection.HTTP_OK) {
|
||||||
|
Log.d(TAG, "Лог успешно отправлен, код ответа: " + responseCode);
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Лог отправлен с предупреждением, код ответа: " + responseCode);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e(TAG, "Ошибка HTTP запроса: " + e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Останавливает executor (вызывать при завершении приложения)
|
||||||
|
*/
|
||||||
|
public static void shutdown() {
|
||||||
|
executor.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user