diff --git a/app/src/main/java/com/grigowashere/aismap/MainActivity.java b/app/src/main/java/com/grigowashere/aismap/MainActivity.java index b08a594..78b1354 100644 --- a/app/src/main/java/com/grigowashere/aismap/MainActivity.java +++ b/app/src/main/java/com/grigowashere/aismap/MainActivity.java @@ -30,6 +30,7 @@ import com.grigowashere.aismap.view.CompassView; import com.grigowashere.aismap.view.CoordinatesDockWidget; import com.grigowashere.aismap.view.BaseDockWidget; import com.grigowashere.aismap.utils.SettingsManager; +import com.grigowashere.aismap.utils.LogSender; import com.yandex.mapkit.mapview.MapView; import java.util.List; import java.util.ArrayList; @@ -631,6 +632,9 @@ public class MainActivity extends AppCompatActivity { if (mapController != null) { mapController.cleanup(); } + + // Останавливаем LogSender + LogSender.shutdown(); } @Override 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 10521d0..7bf49bb 100644 --- a/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java +++ b/app/src/main/java/com/grigowashere/aismap/controllers/NMEAParser.java @@ -3,6 +3,7 @@ package com.grigowashere.aismap.controllers; import android.util.Log; import com.grigowashere.aismap.models.Vessel; import com.grigowashere.aismap.models.AISVessel; +import com.grigowashere.aismap.utils.LogSender; import java.util.List; import java.util.ArrayList; @@ -87,6 +88,9 @@ public class NMEAParser { } Log.d(TAG, "Парсим NMEA: " + cleanedSentence); + // Отправляем NMEA сообщение на внешний ресурс + LogSender.logNMEA(cleanedSentence); + try { // Разбираем сообщение по запятым String[] fields = cleanedSentence.split(","); @@ -657,6 +661,7 @@ public class NMEAParser { // Разбираем AIS сообщение по запятым String[] fields = ais.split(","); + Log.d(TAG, "AIS поля (" + fields.length + "): " + java.util.Arrays.toString(fields)); if (fields.length < 7) { Log.w(TAG, "AIS сообщение слишком короткое: " + ais); return; @@ -676,8 +681,20 @@ public class NMEAParser { // Поле 5: payload (данные) String payload = getField(fields, 5); - // Поле 6: количество бит заполнения - int fillBits = parseIntField(fields, 6, 0); + // Поле 6: количество бит заполнения (может содержать *checksum) + 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]; @@ -883,7 +900,18 @@ public class NMEAParser { ", payloadLength=" + payload.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.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) { listener.onAISVesselUpdated(vessel); @@ -978,6 +1011,7 @@ public class NMEAParser { private void decodeStaticData(String payload) { try { Log.d(TAG, "Декодируем Static Data, payload: " + payload + " (длина: " + payload.length() + ")"); + Log.d(TAG, "Общая длина в битах: " + (payload.length() * 6)); // MMSI (30 бит) - начинается с бита 8 String mmsiBits = decodeAISField(payload, 8, 30); @@ -1040,13 +1074,103 @@ public class NMEAParser { int eta = Integer.parseInt(etaBits, 2); Log.d(TAG, "ETA bits: " + etaBits + " = " + eta); - // Destination (120 бит) - бит 314 - String destBits = decodeAISField(payload, 314, 120); - String destination = decodeAISString(destBits); - Log.d(TAG, "Destination bits: " + destBits + " = '" + destination + "'"); + // Парсим ETA согласно стандарту: 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 + 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 судно AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi)); @@ -1058,8 +1182,14 @@ public class NMEAParser { vessel.setWidth(width); vessel.setDraft(draft); vessel.setDestination(destination); + 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'", + vesselName, callSign, getVesselType(vesselTypeCode), length, width, draft, destination); + LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo); + // Уведомляем слушателя if (listener != null) { 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 координаты */ @@ -1103,38 +1278,46 @@ public class NMEAParser { private String decodeAISString(String bits) { StringBuilder result = new StringBuilder(); Log.d(TAG, "Декодируем AIS строку из битов: " + bits + " (длина: " + bits.length() + ")"); - + for (int i = 0; i < bits.length(); i += 6) { if (i + 6 <= bits.length()) { String charBits = bits.substring(i, i + 6); int value = Integer.parseInt(charBits, 2); - - if (value == 0) { - Log.d(TAG, "Найден конец строки (0)"); - break; // Конец строки - } - + 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); - } else if (value >= 27 && value <= 52) { - decodedChar = (char)('a' + value - 27); - } else if (value >= 53 && value <= 62) { - decodedChar = (char)('0' + value - 53); - } else if (value == 63) { - decodedChar = ' '; + Log.d(TAG, "Диапазон A-Z: " + value + " -> " + decodedChar); + } else if (value >= 49 && value <= 58) { + // Цифры 1-9 (кастомное сопоставление на основе AIS1) + decodedChar = (char)('1' + (value - 49)); + Log.d(TAG, "Диапазон 1-9: " + value + " -> " + decodedChar); + } else if (value == 59) { + // Цифра 0 (кастомное сопоставление) + decodedChar = '0'; + Log.d(TAG, "Декодирован символ (59) -> '0'"); } else if (value == 0) { - decodedChar = '@'; // Специальный символ + // Нулевое значение - конец строки, но не останавливаемся сразу + decodedChar = ' '; // Заменяем на пробел для продолжения + Log.d(TAG, "Найден ноль, заменяем на пробел"); } else { - decodedChar = '?'; // Неизвестный символ - Log.w(TAG, "Неизвестное значение AIS символа: " + value); + // Неизвестный или зарезервированный символ + decodedChar = '?'; + Log.w(TAG, "Неизвестное или зарезервированное значение AIS символа: " + value + " (биты: " + charBits + ")"); } - + result.append(decodedChar); Log.d(TAG, "Декодирован символ: " + charBits + " (" + value + ") -> '" + decodedChar + "'"); } } - + String resultStr = result.toString().trim(); Log.d(TAG, "Результат декодирования строки: '" + 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.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) { listener.onAISVesselUpdated(vessel); @@ -1667,6 +1880,11 @@ public class NMEAParser { 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); + LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo); + // Уведомляем слушателя if (listener != null) { listener.onAISVesselUpdated(vessel); diff --git a/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java index be6326c..a7e4736 100644 --- a/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java @@ -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() { private long lastUpdateTime = 0; - private static final long UPDATE_THROTTLE = 100; // 100мс между обновлениями + private static final long UPDATE_THROTTLE = 50; // 50мс между обновлениями @Override 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) { // Ошибка установки слушателя } diff --git a/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java index 3315c0f..49ccca7 100644 --- a/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java @@ -36,21 +36,29 @@ public class YandexMarkerManager implements MarkerManager { private Runnable cleanupRunnable; 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) { this.context = context; this.mapObjects = mapObjects; this.mapView = mapView; this.cleanupHandler = new Handler(Looper.getMainLooper()); + this.refreshHandler = new Handler(Looper.getMainLooper()); } @Override public void initialize() { startPeriodicCleanup(); + startPeriodicRefresh(); } @Override public void cleanup() { stopPeriodicCleanup(); + stopPeriodicRefresh(); // Удаляем все маркеры 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; + } + } + /** * Очищает устаревшие маркеры */ 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 30bfece..9fde5ff 100644 --- a/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java @@ -92,15 +92,23 @@ public class YandexMarkerWrapper extends MarkerWrapper { private void createMarker() { try { - double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLatitude(); + double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLongitude(); double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude(); + // Сначала создаем иконку + Bitmap iconBitmap = createIconBitmap(); + Point point = new Point(lat, lon); marker = mapObjects.addPlacemark(point); 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(); } } 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; + } + } + /** * Пересоздает маркер с новыми координатами * Этот метод больше не используется - маркеры всегда пересоздаются в менеджере diff --git a/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java b/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java new file mode 100644 index 0000000..bdab5f7 --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/utils/LogSender.java @@ -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(); + } +}