From 2b0afe4d791464e7386e5377dd8bf2382892b8fd Mon Sep 17 00:00:00 2001 From: grigo Date: Fri, 5 Sep 2025 11:35:10 +0300 Subject: [PATCH] Added marker manager Hoping to get a sick day --- .../aismap/maps/MarkerManager.java | 61 +++ .../aismap/maps/MarkerWrapper.java | 101 ++++ .../aismap/maps/YandexMapImpl.java | 517 +++--------------- .../aismap/maps/YandexMarkerManager.java | 295 ++++++++++ .../aismap/maps/YandexMarkerWrapper.java | 405 ++++++++++++++ 5 files changed, 952 insertions(+), 427 deletions(-) create mode 100644 app/src/main/java/com/grigowashere/aismap/maps/MarkerManager.java create mode 100644 app/src/main/java/com/grigowashere/aismap/maps/MarkerWrapper.java create mode 100644 app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java create mode 100644 app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MarkerManager.java b/app/src/main/java/com/grigowashere/aismap/maps/MarkerManager.java new file mode 100644 index 0000000..cfe3f4c --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/MarkerManager.java @@ -0,0 +1,61 @@ +package com.grigowashere.aismap.maps; + +import com.grigowashere.aismap.models.Vessel; +import com.grigowashere.aismap.models.AISVessel; + +/** + * Интерфейс для управления маркерами на карте + * Отделяет логику управления маркерами от конкретной реализации карты + */ +public interface MarkerManager { + + /** + * Инициализация менеджера маркеров + */ + void initialize(); + + /** + * Очистка ресурсов менеджера маркеров + */ + void cleanup(); + + /** + * Добавление или обновление маркера нашего судна + */ + void updateOwnVesselMarker(Vessel vessel); + + /** + * Добавление или обновление маркера AIS судна + */ + void updateAISVesselMarker(AISVessel vessel); + + /** + * Удаление маркера AIS судна + */ + void removeAISVesselMarker(String mmsi); + + /** + * Очистка всех AIS маркеров + */ + void clearAISVesselMarkers(); + + /** + * Установка обработчика кликов по маркерам + */ + void setMarkerClickListener(MapInterface.MarkerClickListener listener); + + /** + * Обновление всех маркеров (например, при повороте карты) + */ + void refreshAllMarkers(); + + /** + * Проверка и восстановление финализированных маркеров + */ + void checkAndRestoreMarkers(); + + /** + * Получение количества активных маркеров + */ + int getActiveMarkerCount(); +} diff --git a/app/src/main/java/com/grigowashere/aismap/maps/MarkerWrapper.java b/app/src/main/java/com/grigowashere/aismap/maps/MarkerWrapper.java new file mode 100644 index 0000000..66ae39e --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/MarkerWrapper.java @@ -0,0 +1,101 @@ +package com.grigowashere.aismap.maps; + +import com.grigowashere.aismap.models.Vessel; +import com.grigowashere.aismap.models.AISVessel; + +/** + * Обертка для маркера с управлением жизненным циклом + * Предотвращает финализацию объектов и обеспечивает стабильную работу + */ +public abstract class MarkerWrapper { + + protected String id; + protected boolean isActive; + protected long lastUpdateTime; + protected long creationTime; + + // Константы для управления жизненным циклом + private static final long MARKER_LIFETIME = 5000; // 5 секунд + private static final long UPDATE_THROTTLE = 200; // 0.2 секунды между обновлениями + + public MarkerWrapper(String id) { + this.id = id; + this.isActive = true; + this.creationTime = System.currentTimeMillis(); + this.lastUpdateTime = creationTime; + } + + /** + * Проверяет, нужно ли обновлять маркер + */ + public boolean shouldUpdate() { + long currentTime = System.currentTimeMillis(); + return (currentTime - lastUpdateTime) >= UPDATE_THROTTLE; + } + + /** + * Проверяет, не устарел ли маркер + */ + public boolean isExpired() { + long currentTime = System.currentTimeMillis(); + return (currentTime - creationTime) >= MARKER_LIFETIME; + } + + /** + * Обновляет время последнего обновления + */ + public void markUpdated() { + this.lastUpdateTime = System.currentTimeMillis(); + } + + /** + * Проверяет, активен ли маркер + */ + public boolean isActive() { + return isActive && !isExpired(); + } + + /** + * Деактивирует маркер + */ + public void deactivate() { + this.isActive = false; + } + + /** + * Получает ID маркера + */ + public String getId() { + return id; + } + + /** + * Абстрактный метод для проверки состояния маркера + */ + public abstract boolean isValid(); + + /** + * Абстрактный метод для обновления позиции маркера + */ + public abstract void updatePosition(double latitude, double longitude); + + /** + * Абстрактный метод для обновления курса маркера + */ + public abstract void updateCourse(double course); + + /** + * Абстрактный метод для удаления маркера + */ + public abstract void remove(); + + /** + * Абстрактный метод для обновления иконки маркера + */ + public abstract void updateIcon(); + + /** + * Абстрактный метод для установки обработчика кликов + */ + public abstract void setClickListener(Runnable clickHandler); +} 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 95e2581..be6326c 100644 --- a/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMapImpl.java @@ -22,6 +22,7 @@ import java.util.Map; /** * Реализация карты для Яндекс.Карт + * Использует новый менеджер маркеров для предотвращения финализации объектов */ public class YandexMapImpl implements MapInterface { @@ -30,35 +31,22 @@ public class YandexMapImpl implements MapInterface { private MapObjectCollection mapObjects; private MarkerClickListener markerClickListener; - private Map aisMarkers; - private Map aisVessels; // Храним ссылки на AISVessel объекты - private com.yandex.mapkit.map.PlacemarkMapObject ownVesselMarker; - private Vessel ownVessel; // Храним ссылку на наше судно - - // Флаги для отслеживания состояния обработчиков - private boolean ownVesselClickListenerSet = false; - private Map aisVesselClickListenersSet = new HashMap<>(); - - // Флаги для предотвращения повторного добавления обработчиков - private Map aisVesselClickListenersAdded = new HashMap<>(); + // Новый менеджер маркеров + private YandexMarkerManager markerManager; // Слушатель поворота карты private com.yandex.mapkit.map.InputListener inputListener; private float lastMapAzimuth = 0.0f; - // Отслеживание двойного клика для AIS судов - private Map lastClickTime = new HashMap<>(); - private static final long DOUBLE_CLICK_DELAY = 300; // 300мс для двойного клика - public YandexMapImpl(Context context, MapView mapView) { this.context = context; this.mapView = mapView; - this.aisMarkers = new HashMap<>(); - this.aisVessels = new HashMap<>(); // Получение коллекции объектов карты try { this.mapObjects = mapView.getMap().getMapObjects().addCollection(); + // Инициализируем менеджер маркеров + this.markerManager = new YandexMarkerManager(context, mapObjects, mapView); } catch (Exception e) { // Ошибка создания коллекции объектов карты } @@ -68,10 +56,21 @@ public class YandexMapImpl implements MapInterface { public void initialize() { // Инициализируем слушатель поворота карты setupCameraListener(); + + // Инициализируем менеджер маркеров + if (markerManager != null) { + markerManager.initialize(); + } } + @Override public void cleanup() { + // Очищаем менеджер маркеров + if (markerManager != null) { + markerManager.cleanup(); + } + // Удаляем слушатель ввода if (inputListener != null && mapView != null) { mapView.getMap().removeInputListener(inputListener); @@ -87,168 +86,44 @@ public class YandexMapImpl implements MapInterface { @Override public void addOwnVesselMarker(Vessel vessel) { - // Сохраняем ссылку на судно - this.ownVessel = vessel; - - // Проверяем валидность координат (исключаем только невалидные значения) - if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) || - Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) { - return; - } - - if (ownVesselMarker != null) { - mapObjects.remove(ownVesselMarker); - } - - Point point = new Point(vessel.getLatitude(), vessel.getLongitude()); - ownVesselMarker = mapObjects.addPlacemark(point); - - if (ownVesselMarker == null) { - return; - } - - // Используем готовую иконку стрелки с учетом курса (синий цвет для нашего судна) - setMarkerIcon(ownVesselMarker, "target", vessel.getCourse(), android.graphics.Color.BLUE); - - // Устанавливаем размер иконки - com.yandex.mapkit.map.IconStyle iconStyle = new com.yandex.mapkit.map.IconStyle(); - iconStyle.setScale(1.0f); // Уменьшаем масштаб иконки - ownVesselMarker.setIconStyle(iconStyle); - - // Устанавливаем обработчик кликов только если он еще не установлен - if (!ownVesselClickListenerSet) { - ownVesselMarker.addTapListener((mapObject, point1) -> { - if (markerClickListener != null && ownVessel != null) { - markerClickListener.onOwnVesselClick(ownVessel); - } - return true; - }); - ownVesselClickListenerSet = true; + if (markerManager != null) { + markerManager.updateOwnVesselMarker(vessel); } } @Override public void updateOwnVesselPosition(Vessel vessel) { - // Обновляем ссылку на судно - this.ownVessel = vessel; - - // Проверяем валидность координат (исключаем только невалидные значения) - if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) || - Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) { - return; - } - - if (ownVesselMarker == null) { - // Создаем маркер нашего судна, если его еще нет - addOwnVesselMarker(vessel); - } else { - // Всегда обновляем иконку с текущим курсом - // Обновляем иконку с новым курсом (синий цвет для нашего судна) - setMarkerIcon(ownVesselMarker, "target", vessel.getCourse(), android.graphics.Color.BLUE); - - // Обновляем позицию маркера - Point newPoint = new Point(vessel.getLatitude(), vessel.getLongitude()); - ownVesselMarker.setGeometry(newPoint); - - // Обработчик клика уже установлен при создании маркера, не добавляем повторно + if (markerManager != null) { + markerManager.updateOwnVesselMarker(vessel); } } @Override public void addAISVesselMarker(AISVessel vessel) { - // Проверяем валидность координат (исключаем только невалидные значения) - if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) || - Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) { - return; + if (markerManager != null) { + markerManager.updateAISVesselMarker(vessel); } - - Point point = new Point(vessel.getLatitude(), vessel.getLongitude()); - com.yandex.mapkit.map.PlacemarkMapObject marker = mapObjects.addPlacemark(point); - - if (marker == null) { - return; - } - - // Сохраняем ссылку на судно - aisVessels.put(vessel.getMmsi(), vessel); - - // Используем готовую иконку стрелки для AIS судов с учетом курса и цвета - int vesselColor = getVesselColor(vessel); - setMarkerIcon(marker, "target", vessel.getCourse(), vesselColor, vessel.isSelected()); - - // Устанавливаем размер иконки - com.yandex.mapkit.map.IconStyle iconStyle = new com.yandex.mapkit.map.IconStyle(); - iconStyle.setScale(1.0f); // Уменьшаем масштаб иконки - marker.setIconStyle(iconStyle); - - // Установка обработчика кликов только если он еще не установлен - String mmsi = vessel.getMmsi(); - if (!aisVesselClickListenersAdded.containsKey(mmsi) || !aisVesselClickListenersAdded.get(mmsi)) { - marker.addTapListener((mapObject, point1) -> { - // Проверяем двойной клик - long currentTime = System.currentTimeMillis(); - Long lastTime = lastClickTime.get(mmsi); - - if (lastTime != null && (currentTime - lastTime) < DOUBLE_CLICK_DELAY) { - // Двойной клик - вызываем BottomSheet - if (markerClickListener != null) { - markerClickListener.onAISVesselClick(vessel); - } - lastClickTime.remove(mmsi); // Сбрасываем для следующего двойного клика - } else { - // Одиночный клик - выделяем/снимаем выделение - toggleVesselSelection(vessel); - lastClickTime.put(mmsi, currentTime); - } - - return true; - }); - aisVesselClickListenersAdded.put(mmsi, true); - } - - aisMarkers.put(vessel.getMmsi(), marker); } @Override public void updateAISVesselPosition(AISVessel vessel) { - // Обновляем ссылку на судно - aisVessels.put(vessel.getMmsi(), vessel); - - com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.get(vessel.getMmsi()); - if (marker != null) { - Point newPoint = new Point(vessel.getLatitude(), vessel.getLongitude()); - marker.setGeometry(newPoint); - - // Всегда обновляем курс маркера - int vesselColor = getVesselColor(vessel); - setMarkerIcon(marker, "target", vessel.getCourse(), vesselColor, vessel.isSelected()); - - // Обработчик клика уже установлен при создании маркера, не добавляем повторно + if (markerManager != null) { + markerManager.updateAISVesselMarker(vessel); } } @Override public void removeAISVesselMarker(String mmsi) { - com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.remove(mmsi); - if (marker != null) { - mapObjects.remove(marker); + if (markerManager != null) { + markerManager.removeAISVesselMarker(mmsi); } - // Удаляем ссылку на судно - aisVessels.remove(mmsi); - // Удаляем флаги обработчиков кликов - aisVesselClickListenersSet.remove(mmsi); - aisVesselClickListenersAdded.remove(mmsi); } @Override public void clearAISVesselMarkers() { - for (com.yandex.mapkit.map.PlacemarkMapObject marker : aisMarkers.values()) { - mapObjects.remove(marker); + if (markerManager != null) { + markerManager.clearAISVesselMarkers(); } - aisMarkers.clear(); - aisVessels.clear(); - aisVesselClickListenersSet.clear(); - aisVesselClickListenersAdded.clear(); } @Override @@ -286,159 +161,62 @@ public class YandexMapImpl implements MapInterface { public void setMarkerClickListener(MarkerClickListener listener) { this.markerClickListener = listener; - // Переустанавливаем обработчики кликов для всех существующих маркеров - updateAllMarkerClickListeners(); + // Устанавливаем обработчик в менеджере маркеров + if (markerManager != null) { + markerManager.setMarkerClickListener(listener); + } } /** * Обновляет обработчики кликов для всех существующих маркеров * Этот метод переустанавливает обработчики для всех маркеров */ - private void updateAllMarkerClickListeners() { - // Обработчик для маркера нашего судна уже установлен при создании, не добавляем повторно - - // Обработчики для AIS маркеров уже установлены при создании, не добавляем повторно - } - - /** - * Создание повернутой цветной иконки из ресурса - */ - private Bitmap createRotatedIconFromResource(int resourceId, double course, int color) { - return createRotatedIconFromResource(resourceId, course, color, false); - } - - /** - * Создание повернутой цветной иконки из ресурса с поддержкой выделения - */ - private Bitmap createRotatedIconFromResource(int resourceId, double course, int color, boolean isSelected) { - try { - // Получаем drawable из ресурса - Drawable drawable = context.getResources().getDrawable(resourceId, null); - if (drawable == null) { - return null; - } - - // Применяем цвет к drawable - if (color != 0) { - drawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN); - } - - // Получаем оригинальные размеры drawable - int originalWidth = drawable.getIntrinsicWidth(); - int originalHeight = drawable.getIntrinsicHeight(); - - // Если размеры не определены, используем стандартные - if (originalWidth <= 0) originalWidth = 32; - if (originalHeight <= 0) originalHeight = 48; - - // Масштабируем размеры (уменьшаем еще больше) - float scale = 0.3f; // Уменьшаем в 3.3 раза - int width = (int) (originalWidth * scale); - int height = (int) (originalHeight * scale); - - // Получаем азимут карты (поворот карты) - float mapAzimuth = 0.0f; - try { - CameraPosition cameraPosition = mapView.getMap().getCameraPosition(); - mapAzimuth = cameraPosition.getAzimuth(); - } catch (Exception e) { - // Не удалось получить азимут карты - } - - // Создаем bitmap минимального размера для уменьшения хитбокса - int bitmapSize = Math.max(width, height) + 8; // Добавляем только небольшой отступ - Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - // Поворачиваем маркер на курс судна с учетом поворота карты - // Курс судна - это направление относительно севера - // Азимут карты - это поворот карты относительно севера - // Итоговый поворот = курс судна - азимут карты (чтобы маркер оставался относительно севера) - float rotationAngle = (float) (course - mapAzimuth); - - // Центрируем drawable в bitmap - int centerX = bitmapSize / 2; - int centerY = bitmapSize / 2; - int left = centerX - width / 2; - int top = centerY - height / 2; - - // Устанавливаем границы для drawable - drawable.setBounds(left, top, left + width, top + height); - - // Поворачиваем canvas на курс - canvas.save(); - canvas.rotate(rotationAngle, centerX, centerY); - - // Рисуем drawable - drawable.draw(canvas); - - canvas.restore(); - - // Если судно выделено, добавляем рамку выделения - if (isSelected) { - // Получаем drawable для рамки выделения - Drawable selectionDrawable = context.getResources().getDrawable( - context.getResources().getIdentifier("chosentarget", "drawable", context.getPackageName()), null); - - if (selectionDrawable != null) { - // Масштабируем рамку выделения - int selectionSize = Math.max(width, height) + 16; // Рамка немного больше - int selectionLeft = centerX - selectionSize / 2; - int selectionTop = centerY - selectionSize / 2; - - selectionDrawable.setBounds(selectionLeft, selectionTop, - selectionLeft + selectionSize, selectionTop + selectionSize); - - // Рисуем рамку выделения - selectionDrawable.draw(canvas); - } - } - - return bitmap; - } catch (Exception e) { - return null; + public void refreshMarkerClickListeners() { + if (markerManager != null) { + markerManager.checkAndRestoreMarkers(); } } /** - * Создание иконки судна + * Перерисовывает все маркеры с учетом текущего азимута карты + * Вызывается при повороте карты */ - private Bitmap createVesselIcon(int color, double course) { - try { - int size = 64; // Увеличиваем размер для лучшей видимости - Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - Paint paint = new Paint(); - paint.setColor(color); - paint.setStyle(Paint.Style.FILL); - paint.setAntiAlias(true); - paint.setStrokeWidth(3.0f); - - // Рисуем треугольник-стрелку, направленную вверх (по умолчанию) - android.graphics.Path path = new android.graphics.Path(); - path.moveTo(size / 2f, 0); // вершина - path.lineTo(size * 0.1f, size * 0.8f); // левый нижний угол - path.lineTo(size * 0.3f, size * 0.6f); // левая внутренняя точка - path.lineTo(size * 0.3f, size * 0.9f); // левая нижняя точка - path.lineTo(size * 0.7f, size * 0.9f); // правая нижняя точка - path.lineTo(size * 0.7f, size * 0.6f); // правая внутренняя точка - path.lineTo(size * 0.9f, size * 0.8f); // правый нижний угол - path.close(); - - // Поворачиваем стрелку на курс (курс 0° = стрелка направлена вверх) - // В морской навигации курс 0° = север, 90° = восток, 180° = юг, 270° = запад - canvas.save(); - canvas.rotate((float) course, size / 2f, size / 2f); - canvas.drawPath(path, paint); - canvas.restore(); - - return bitmap; - } catch (Exception e) { - return null; + public void refreshAllMarkers() { + if (markerManager != null) { + markerManager.refreshAllMarkers(); } } + /** + * Обновляет все маркеры при повороте карты + * Вызывается из слушателя поворота карты + */ + public void onMapRotationChanged() { + if (markerManager != null) { + markerManager.refreshAllMarkers(); + } + } + + /** + * Проверяет и восстанавливает финализированные маркеры + */ + public void checkAndRestoreMarkers() { + if (markerManager != null) { + markerManager.checkAndRestoreMarkers(); + } + } + + /** + * Получает количество активных маркеров + */ + public int getActiveMarkerCount() { + if (markerManager != null) { + return markerManager.getActiveMarkerCount(); + } + return 0; + } + + /** * Получение MapView для использования в layout */ @@ -446,79 +224,6 @@ public class YandexMapImpl implements MapInterface { return mapView; } - /** - * Принудительно пересоздает маркер нашего судна с иконкой - */ - public void recreateOwnVesselMarker(Vessel vessel) { - if (ownVesselMarker != null) { - mapObjects.remove(ownVesselMarker); - ownVesselMarker = null; - } - addOwnVesselMarker(vessel); - } - - /** - * Обновляет обработчики кликов для всех маркеров - * Вызывается после закрытия BottomSheet для восстановления функциональности - */ - public void refreshMarkerClickListeners() { - updateAllMarkerClickListeners(); - } - - /** - * Устанавливает иконку для маркера с fallback - */ - private void setMarkerIcon(com.yandex.mapkit.map.PlacemarkMapObject marker, String iconName, double course) { - setMarkerIcon(marker, iconName, course, 0); // По умолчанию без цвета - } - - /** - * Устанавливает цветную иконку для маркера с fallback - */ - private void setMarkerIcon(com.yandex.mapkit.map.PlacemarkMapObject marker, String iconName, double course, int color) { - setMarkerIcon(marker, iconName, course, color, false); - } - - /** - * Устанавливает цветную иконку для маркера с поддержкой выделения - */ - private void setMarkerIcon(com.yandex.mapkit.map.PlacemarkMapObject marker, String iconName, double course, int color, boolean isSelected) { - try { - // Сначала пробуем использовать ресурс с поворотом - int iconResId = context.getResources().getIdentifier(iconName, "drawable", context.getPackageName()); - - if (iconResId != 0) { - Bitmap rotatedBitmap = createRotatedIconFromResource(iconResId, course, color, isSelected); - if (rotatedBitmap != null) { - marker.setIcon(ImageProvider.fromBitmap(rotatedBitmap)); - return; - } else { - marker.setIcon(ImageProvider.fromResource(context, iconResId)); - return; - } - } - - // Если ресурс не найден, создаем программную иконку с учетом курса - Bitmap iconBitmap = createVesselIcon(android.graphics.Color.BLUE, course); - if (iconBitmap != null) { - marker.setIcon(ImageProvider.fromBitmap(iconBitmap)); - } else { - // Создаем простую иконку как fallback - marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass)); - } - } catch (Exception e) { - // Создаем простую иконку как fallback - marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass)); - } - - // После установки иконки проверяем, что обработчик клика все еще работает - // Это может помочь с проблемами, когда установка иконки нарушает обработчики - - // Обработчик клика для нашего судна уже установлен при создании маркера, не добавляем повторно - - // Обработчики кликов уже установлены при создании маркеров, не добавляем повторно - } - /** * Настройка слушателя поворота карты */ @@ -541,71 +246,29 @@ public class YandexMapImpl implements MapInterface { // Включаем жесты поворота карты mapView.getMap().setRotateGesturesEnabled(true); + + // Добавляем слушатель изменений камеры для обновления маркеров при повороте + mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() { + private long lastUpdateTime = 0; + private static final long UPDATE_THROTTLE = 100; // 100мс между обновлениями + + @Override + public void onCameraPositionChanged(com.yandex.mapkit.map.Map map, + com.yandex.mapkit.map.CameraPosition cameraPosition, + com.yandex.mapkit.map.CameraUpdateReason reason, + boolean finished) { + + // Обновляем маркеры в реальном времени с throttling + long currentTime = System.currentTimeMillis(); + if (currentTime - lastUpdateTime >= UPDATE_THROTTLE) { + onMapRotationChanged(); + lastUpdateTime = currentTime; + } + } + }); } catch (Exception e) { // Ошибка установки слушателя } } - /** - * Перерисовывает все маркеры с учетом текущего азимута карты - * Вызывается при повороте карты - */ - public void refreshAllMarkers() { - // Перерисовываем маркер нашего судна (синий цвет) - if (ownVesselMarker != null && ownVessel != null) { - setMarkerIcon(ownVesselMarker, "target", ownVessel.getCourse(), android.graphics.Color.BLUE); - } - - // Перерисовываем все AIS маркеры с их цветами - for (Map.Entry entry : aisMarkers.entrySet()) { - String mmsi = entry.getKey(); - com.yandex.mapkit.map.PlacemarkMapObject marker = entry.getValue(); - AISVessel vessel = aisVessels.get(mmsi); - - if (marker != null && vessel != null) { - int vesselColor = getVesselColor(vessel); - setMarkerIcon(marker, "target", vessel.getCourse(), vesselColor, vessel.isSelected()); - } - } - } - - /** - * Переключает выделение AIS судна - */ - private void toggleVesselSelection(AISVessel vessel) { - vessel.setSelected(!vessel.isSelected()); - - // Обновляем иконку маркера с учетом состояния выделения - com.yandex.mapkit.map.PlacemarkMapObject marker = aisMarkers.get(vessel.getMmsi()); - if (marker != null) { - int vesselColor = getVesselColor(vessel); - setMarkerIcon(marker, "target", vessel.getCourse(), vesselColor, vessel.isSelected()); - } - } - - /** - * Получает цвет для AIS судна в зависимости от его статуса - */ - private int getVesselColor(AISVessel vessel) { - // Можно настроить цвета в зависимости от параметров судна - // Используем navigation status из AIS данных - String navStatus = vessel.getNavigationalStatus(); - if (navStatus != null) { - switch (navStatus.toLowerCase()) { - case "under way using engine": - case "under way": - return android.graphics.Color.GREEN; - case "at anchor": - return android.graphics.Color.YELLOW; - case "moored": - return android.graphics.Color.BLUE; - case "not under command": - case "restricted manoeuvrability": - return android.graphics.Color.RED; - default: - return android.graphics.Color.WHITE; - } - } - return android.graphics.Color.WHITE; // По умолчанию белый - } } diff --git a/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java new file mode 100644 index 0000000..3315c0f --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerManager.java @@ -0,0 +1,295 @@ +package com.grigowashere.aismap.maps; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; + +import com.grigowashere.aismap.models.Vessel; +import com.grigowashere.aismap.models.AISVessel; +import com.yandex.mapkit.map.MapObjectCollection; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Менеджер маркеров для Yandex Maps + * Управляет жизненным циклом маркеров и предотвращает их финализацию + */ +public class YandexMarkerManager implements MarkerManager { + + private static final String TAG = "YandexMarkerManager"; + + private Context context; + private MapObjectCollection mapObjects; + private com.yandex.mapkit.mapview.MapView mapView; + private MapInterface.MarkerClickListener markerClickListener; + + // Кеш маркеров с управлением жизненным циклом + private Map markerCache = new ConcurrentHashMap<>(); + private YandexMarkerWrapper ownVesselMarker; + + // Периодическая очистка устаревших маркеров + private Handler cleanupHandler; + private Runnable cleanupRunnable; + private static final long CLEANUP_INTERVAL = 10000; // 10 секунд + + 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()); + } + + @Override + public void initialize() { + startPeriodicCleanup(); + } + + @Override + public void cleanup() { + stopPeriodicCleanup(); + + // Удаляем все маркеры + for (YandexMarkerWrapper marker : markerCache.values()) { + marker.remove(); + } + markerCache.clear(); + + if (ownVesselMarker != null) { + ownVesselMarker.remove(); + ownVesselMarker = null; + } + } + + @Override + public void updateOwnVesselMarker(Vessel vessel) { + if (vessel == null) { + return; + } + + // Проверяем валидность координат + if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) || + Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) { + return; + } + + // ВСЕГДА пересоздаем маркер для предотвращения финализации + if (ownVesselMarker != null) { + ownVesselMarker.remove(); + } + + // Создаем новый маркер + ownVesselMarker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, "own_vessel"); + if (markerClickListener != null) { + ownVesselMarker.setClickListener(() -> { + if (markerClickListener != null) { + markerClickListener.onOwnVesselClick(vessel); + } + }); + } + } + + @Override + public void updateAISVesselMarker(AISVessel vessel) { + if (vessel == null || vessel.getMmsi() == null) { + return; + } + + // Проверяем валидность координат + if (Double.isNaN(vessel.getLatitude()) || Double.isNaN(vessel.getLongitude()) || + Double.isInfinite(vessel.getLatitude()) || Double.isInfinite(vessel.getLongitude())) { + return; + } + + String mmsi = vessel.getMmsi(); + YandexMarkerWrapper marker = markerCache.get(mmsi); + + // ВСЕГДА пересоздаем маркер для предотвращения финализации + if (marker != null) { + marker.remove(); + } + + // Создаем новый маркер + marker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, mmsi); + markerCache.put(mmsi, marker); + + if (markerClickListener != null) { + marker.setClickListener(() -> { + if (markerClickListener != null) { + markerClickListener.onAISVesselClick(vessel); + } + }); + } + } + + @Override + public void removeAISVesselMarker(String mmsi) { + YandexMarkerWrapper marker = markerCache.remove(mmsi); + if (marker != null) { + marker.remove(); + } + } + + @Override + public void clearAISVesselMarkers() { + for (YandexMarkerWrapper marker : markerCache.values()) { + marker.remove(); + } + markerCache.clear(); + } + + @Override + public void setMarkerClickListener(MapInterface.MarkerClickListener listener) { + this.markerClickListener = listener; + + // Устанавливаем обработчики для существующих маркеров + if (ownVesselMarker != null && ownVesselMarker.isValid()) { + ownVesselMarker.setClickListener(() -> { + if (markerClickListener != null && ownVesselMarker.getVessel() != null) { + markerClickListener.onOwnVesselClick(ownVesselMarker.getVessel()); + } + }); + } + + for (YandexMarkerWrapper marker : markerCache.values()) { + if (marker.isValid()) { + marker.setClickListener(() -> { + if (markerClickListener != null && marker.getAISVessel() != null) { + markerClickListener.onAISVesselClick(marker.getAISVessel()); + } + }); + } + } + } + + @Override + public void refreshAllMarkers() { + // При повороте карты пересоздаем все маркеры + // Это гарантирует правильную ориентацию относительно севера + + // Пересоздаем маркер нашего судна + if (ownVesselMarker != null) { + Vessel vessel = ownVesselMarker.getVessel(); + if (vessel != null) { + ownVesselMarker.remove(); + updateOwnVesselMarker(vessel); + } + } + + // Пересоздаем все AIS маркеры + Map vesselsToRecreate = new HashMap<>(); + for (Map.Entry entry : markerCache.entrySet()) { + YandexMarkerWrapper marker = entry.getValue(); + AISVessel vessel = marker.getAISVessel(); + if (vessel != null) { + marker.remove(); + vesselsToRecreate.put(entry.getKey(), vessel); + } + } + + // Очищаем кеш и пересоздаем маркеры + markerCache.clear(); + for (Map.Entry entry : vesselsToRecreate.entrySet()) { + updateAISVesselMarker(entry.getValue()); + } + } + + @Override + public void checkAndRestoreMarkers() { + // Проверяем маркер нашего судна + if (ownVesselMarker != null && !ownVesselMarker.isValid()) { + Vessel vessel = ownVesselMarker.getVessel(); + if (vessel != null) { + ownVesselMarker.remove(); + updateOwnVesselMarker(vessel); + } + } + + // Проверяем AIS маркеры + Set toRemove = new HashSet<>(); + for (Map.Entry entry : markerCache.entrySet()) { + YandexMarkerWrapper marker = entry.getValue(); + if (!marker.isValid()) { + AISVessel vessel = marker.getAISVessel(); + if (vessel != null) { + marker.remove(); + toRemove.add(entry.getKey()); + updateAISVesselMarker(vessel); + } else { + toRemove.add(entry.getKey()); + } + } + } + + // Удаляем невалидные маркеры + for (String mmsi : toRemove) { + markerCache.remove(mmsi); + } + } + + @Override + public int getActiveMarkerCount() { + int count = 0; + if (ownVesselMarker != null && ownVesselMarker.isValid()) { + count++; + } + for (YandexMarkerWrapper marker : markerCache.values()) { + if (marker.isValid()) { + count++; + } + } + return count; + } + + /** + * Запускает периодическую очистку устаревших маркеров + */ + private void startPeriodicCleanup() { + cleanupRunnable = new Runnable() { + @Override + public void run() { + cleanupExpiredMarkers(); + cleanupHandler.postDelayed(this, CLEANUP_INTERVAL); + } + }; + cleanupHandler.post(cleanupRunnable); + } + + /** + * Останавливает периодическую очистку + */ + private void stopPeriodicCleanup() { + if (cleanupRunnable != null) { + cleanupHandler.removeCallbacks(cleanupRunnable); + cleanupRunnable = null; + } + } + + /** + * Очищает устаревшие маркеры + */ + private void cleanupExpiredMarkers() { + // Очищаем AIS маркеры + Set toRemove = new HashSet<>(); + for (Map.Entry entry : markerCache.entrySet()) { + YandexMarkerWrapper marker = entry.getValue(); + if (marker.isExpired() || !marker.isValid()) { + marker.remove(); + toRemove.add(entry.getKey()); + } + } + + for (String mmsi : toRemove) { + markerCache.remove(mmsi); + } + + // Проверяем маркер нашего судна + if (ownVesselMarker != null && (ownVesselMarker.isExpired() || !ownVesselMarker.isValid())) { + ownVesselMarker.remove(); + ownVesselMarker = 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 new file mode 100644 index 0000000..30bfece --- /dev/null +++ b/app/src/main/java/com/grigowashere/aismap/maps/YandexMarkerWrapper.java @@ -0,0 +1,405 @@ +package com.grigowashere.aismap.maps; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; + +import com.grigowashere.aismap.models.Vessel; +import com.grigowashere.aismap.models.AISVessel; +import com.yandex.mapkit.geometry.Point; +import com.yandex.mapkit.map.PlacemarkMapObject; +import com.yandex.mapkit.map.MapObjectCollection; +import com.yandex.runtime.image.ImageProvider; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Обертка для маркера Yandex Maps с управлением жизненным циклом + */ +public class YandexMarkerWrapper extends MarkerWrapper { + + private Context context; + private PlacemarkMapObject marker; + private MapObjectCollection mapObjects; + private Vessel vessel; + private AISVessel aisVessel; + private boolean isOwnVessel; + private AtomicBoolean clickListenerSet = new AtomicBoolean(false); + private Runnable clickHandler; + + // Ссылка на MapView для получения азимута карты + private com.yandex.mapkit.mapview.MapView mapView; + + // Кешированные данные для предотвращения лишних обновлений + private double lastLatitude = Double.NaN; + private double lastLongitude = Double.NaN; + private double lastCourse = Double.NaN; + private int lastColor = -1; + private boolean lastSelected = false; + + // Кеш иконок для быстрого отображения + private Bitmap cachedIconBitmap; + private double cachedIconCourse = Double.NaN; + private int cachedIconColor = -1; + private boolean cachedIconSelected = false; + + public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects, + com.yandex.mapkit.mapview.MapView mapView, Vessel vessel, String id) { + super(id); + this.context = context; + this.mapObjects = mapObjects; + this.mapView = mapView; + this.vessel = vessel; + this.isOwnVessel = true; + // Предварительно создаем иконку + preloadIcon(); + createMarker(); + } + + public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects, + com.yandex.mapkit.mapview.MapView mapView, AISVessel vessel, String id) { + super(id); + this.context = context; + this.mapObjects = mapObjects; + this.mapView = mapView; + this.aisVessel = vessel; + this.isOwnVessel = false; + // Предварительно создаем иконку + preloadIcon(); + createMarker(); + } + + /** + * Предварительно создает иконку для быстрого отображения + */ + private void preloadIcon() { + try { + double course = isOwnVessel ? vessel.getCourse() : aisVessel.getCourse(); + int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor(); + boolean selected = !isOwnVessel && aisVessel.isSelected(); + + cachedIconBitmap = createRotatedIcon(course, color, selected); + cachedIconCourse = course; + cachedIconColor = color; + cachedIconSelected = selected; + } catch (Exception e) { + // Ошибка предварительной загрузки иконки + cachedIconBitmap = null; + } + } + + private void createMarker() { + try { + double lat = isOwnVessel ? vessel.getLatitude() : aisVessel.getLatitude(); + double lon = isOwnVessel ? vessel.getLongitude() : aisVessel.getLongitude(); + + Point point = new Point(lat, lon); + marker = mapObjects.addPlacemark(point); + + if (marker != null) { + // Сразу устанавливаем иконку, чтобы избежать плейсхолдера + setIconImmediately(); + setupClickListener(); + } + } catch (Exception e) { + // Ошибка создания маркера + deactivate(); + } + } + + /** + * Пересоздает маркер с новыми координатами + * Этот метод больше не используется - маркеры всегда пересоздаются в менеджере + */ + private void recreateMarker(double latitude, double longitude) { + // Метод оставлен для совместимости, но не используется + } + + /** + * Устанавливает иконку немедленно без проверок + */ + private void setIconImmediately() { + try { + double course = isOwnVessel ? vessel.getCourse() : aisVessel.getCourse(); + int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor(); + boolean selected = !isOwnVessel && aisVessel.isSelected(); + + // Проверяем кеш иконки + Bitmap iconBitmap = null; + if (Double.compare(course, cachedIconCourse) == 0 && + color == cachedIconColor && + selected == cachedIconSelected && + cachedIconBitmap != null) { + // Используем кешированную иконку + iconBitmap = cachedIconBitmap; + } else { + // Создаем новую иконку + iconBitmap = createRotatedIcon(course, color, selected); + if (iconBitmap != null) { + // Кешируем иконку + cachedIconBitmap = iconBitmap; + cachedIconCourse = course; + cachedIconColor = color; + cachedIconSelected = selected; + } + } + + if (iconBitmap != null) { + marker.setIcon(ImageProvider.fromBitmap(iconBitmap)); + } else { + // Fallback иконка если не удалось создать повернутую + marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass)); + } + + // Обновляем кешированные значения + lastCourse = course; + lastColor = color; + lastSelected = selected; + } catch (Exception e) { + // Ошибка установки иконки - используем fallback + try { + marker.setIcon(ImageProvider.fromResource(context, android.R.drawable.ic_menu_compass)); + } catch (Exception ex) { + // Игнорируем ошибки fallback + } + } + } + + @Override + public boolean isValid() { + try { + if (marker == null) { + return false; + } + // Пробуем получить геометрию для проверки состояния + marker.getGeometry(); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void updatePosition(double latitude, double longitude) { + // Этот метод больше не используется - маркеры всегда пересоздаются + // Оставляем для совместимости с интерфейсом + } + + @Override + public void updateCourse(double course) { + // Этот метод больше не используется - маркеры всегда пересоздаются + // Оставляем для совместимости с интерфейсом + } + + @Override + public void remove() { + try { + if (marker != null) { + mapObjects.remove(marker); + } + } catch (Exception e) { + // Игнорируем ошибки при удалении + } finally { + deactivate(); + } + } + + @Override + public void updateIcon() { + // Этот метод больше не используется - маркеры всегда пересоздаются + // Оставляем для совместимости с интерфейсом + } + + @Override + public void setClickListener(Runnable clickHandler) { + this.clickHandler = clickHandler; + setupClickListener(); + } + + private void setupClickListener() { + if (marker == null || clickHandler == null) { + return; + } + + // Сбрасываем флаг для возможности повторной установки + clickListenerSet.set(false); + + try { + marker.addTapListener((mapObject, point) -> { + try { + if (mapObject != null && clickHandler != null) { + clickHandler.run(); + } + return true; + } catch (Exception e) { + return false; + } + }); + clickListenerSet.set(true); + } catch (Exception e) { + // Ошибка установки обработчика кликов + clickListenerSet.set(false); + } + } + + private Bitmap createRotatedIcon(double course, int color, boolean isSelected) { + try { + // Получаем drawable из ресурса + int iconResId = context.getResources().getIdentifier("target", "drawable", context.getPackageName()); + if (iconResId == 0) { + return createSimpleIcon(color, course); + } + + Drawable drawable = context.getResources().getDrawable(iconResId, null); + if (drawable == null) { + return createSimpleIcon(color, course); + } + + // Применяем цвет + if (color != 0) { + drawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN); + } + + // Получаем размеры + int originalWidth = drawable.getIntrinsicWidth(); + int originalHeight = drawable.getIntrinsicHeight(); + + if (originalWidth <= 0) originalWidth = 32; + if (originalHeight <= 0) originalHeight = 48; + + // Масштабируем + float scale = 0.3f; + int width = (int) (originalWidth * scale); + int height = (int) (originalHeight * scale); + + // Создаем bitmap + int bitmapSize = Math.max(width, height) + 8; + Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + // Получаем азимут карты (поворот карты) + float mapAzimuth = 0.0f; + try { + if (mapView != null) { + com.yandex.mapkit.map.CameraPosition cameraPosition = mapView.getMap().getCameraPosition(); + mapAzimuth = cameraPosition.getAzimuth(); + } + } catch (Exception e) { + // Не удалось получить азимут карты, используем 0 + } + + // Поворачиваем маркер на курс судна с учетом поворота карты + // Курс судна - это направление относительно севера + // Азимут карты - это поворот карты относительно севера + // Итоговый поворот = курс судна - азимут карты (чтобы маркер оставался относительно севера) + float rotationAngle = (float) (course - mapAzimuth); + + int centerX = bitmapSize / 2; + int centerY = bitmapSize / 2; + int left = centerX - width / 2; + int top = centerY - height / 2; + + drawable.setBounds(left, top, left + width, top + height); + + canvas.save(); + canvas.rotate(rotationAngle, centerX, centerY); + drawable.draw(canvas); + canvas.restore(); + + // Добавляем рамку выделения если нужно + if (isSelected) { + addSelectionFrame(canvas, centerX, centerY, Math.max(width, height)); + } + + return bitmap; + } catch (Exception e) { + return createSimpleIcon(color, course); + } + } + + private Bitmap createSimpleIcon(int color, double course) { + try { + int size = 32; + Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Paint paint = new Paint(); + paint.setColor(color); + paint.setStyle(Paint.Style.FILL); + paint.setAntiAlias(true); + + // Рисуем треугольник + android.graphics.Path path = new android.graphics.Path(); + path.moveTo(size / 2f, 0); + path.lineTo(size * 0.1f, size * 0.8f); + path.lineTo(size * 0.9f, size * 0.8f); + path.close(); + + canvas.save(); + canvas.rotate((float) course, size / 2f, size / 2f); + canvas.drawPath(path, paint); + canvas.restore(); + + return bitmap; + } catch (Exception e) { + return null; + } + } + + private void addSelectionFrame(Canvas canvas, int centerX, int centerY, int size) { + try { + int iconResId = context.getResources().getIdentifier("chosentarget", "drawable", context.getPackageName()); + if (iconResId == 0) return; + + Drawable selectionDrawable = context.getResources().getDrawable(iconResId, null); + if (selectionDrawable == null) return; + + int selectionSize = size + 16; + int selectionLeft = centerX - selectionSize / 2; + int selectionTop = centerY - selectionSize / 2; + + selectionDrawable.setBounds(selectionLeft, selectionTop, + selectionLeft + selectionSize, selectionTop + selectionSize); + selectionDrawable.draw(canvas); + } catch (Exception e) { + // Игнорируем ошибки рамки выделения + } + } + + private int getVesselColor() { + if (aisVessel == null) return android.graphics.Color.WHITE; + + String navStatus = aisVessel.getNavigationalStatus(); + if (navStatus != null) { + switch (navStatus.toLowerCase()) { + case "under way using engine": + case "under way": + return android.graphics.Color.GREEN; + case "at anchor": + return android.graphics.Color.YELLOW; + case "moored": + return android.graphics.Color.BLUE; + case "not under command": + case "restricted manoeuvrability": + return android.graphics.Color.RED; + default: + return android.graphics.Color.WHITE; + } + } + return android.graphics.Color.WHITE; + } + + public Vessel getVessel() { + return vessel; + } + + public AISVessel getAISVessel() { + return aisVessel; + } + + public boolean isOwnVessel() { + return isOwnVessel; + } +}