generated from Grigo/AndroidTemplate
Major architecture update + testink giteawebhook
This commit is contained in:
@@ -4,6 +4,8 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
@@ -27,6 +29,9 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
|||||||
private Runnable tickerRunnable;
|
private Runnable tickerRunnable;
|
||||||
private android.widget.TextView textEmptyState;
|
private android.widget.TextView textEmptyState;
|
||||||
private android.widget.TextView textTargetCount;
|
private android.widget.TextView textTargetCount;
|
||||||
|
private android.widget.EditText editSearch;
|
||||||
|
private final java.util.List<AISVesselEntity> fullList = new java.util.ArrayList<>();
|
||||||
|
private String currentQuery = "";
|
||||||
|
|
||||||
// Данные нашего корабля
|
// Данные нашего корабля
|
||||||
private double ourLatitude = 0;
|
private double ourLatitude = 0;
|
||||||
@@ -46,10 +51,25 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
|||||||
recyclerView = findViewById(R.id.recycler_ais_targets);
|
recyclerView = findViewById(R.id.recycler_ais_targets);
|
||||||
textEmptyState = findViewById(R.id.text_empty_state);
|
textEmptyState = findViewById(R.id.text_empty_state);
|
||||||
textTargetCount = findViewById(R.id.text_target_count);
|
textTargetCount = findViewById(R.id.text_target_count);
|
||||||
|
editSearch = findViewById(R.id.edit_search);
|
||||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
adapter = new AisTargetsAdapter(new ArrayList<>(), this);
|
adapter = new AisTargetsAdapter(new ArrayList<>(), this);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
editSearch.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
currentQuery = s != null ? s.toString().trim() : "";
|
||||||
|
applyFilterAndUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) { }
|
||||||
|
});
|
||||||
|
|
||||||
repository.observeAllAIS().observe(this, new Observer<List<AISVesselEntity>>() {
|
repository.observeAllAIS().observe(this, new Observer<List<AISVesselEntity>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<AISVesselEntity> entities) {
|
public void onChanged(List<AISVesselEntity> entities) {
|
||||||
@@ -57,23 +77,12 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
|||||||
if (entities != null) {
|
if (entities != null) {
|
||||||
java.util.Collections.sort(entities, (a, b) -> a.mmsi.compareTo(b.mmsi));
|
java.util.Collections.sort(entities, (a, b) -> a.mmsi.compareTo(b.mmsi));
|
||||||
}
|
}
|
||||||
adapter.submitList(entities);
|
fullList.clear();
|
||||||
|
if (entities != null) fullList.addAll(entities);
|
||||||
|
applyFilterAndUpdate();
|
||||||
|
|
||||||
// Обновляем данные нашего корабля в адаптере
|
// Обновляем данные нашего корабля в адаптере
|
||||||
adapter.updateOurVesselData(ourLatitude, ourLongitude, ourCourse);
|
adapter.updateOurVesselData(ourLatitude, ourLongitude, ourCourse);
|
||||||
|
|
||||||
// Обновляем счетчик целей
|
|
||||||
int targetCount = entities != null ? entities.size() : 0;
|
|
||||||
textTargetCount.setText("AIS цели: " + targetCount);
|
|
||||||
|
|
||||||
// Показываем/скрываем сообщение о пустом состоянии
|
|
||||||
if (entities == null || entities.isEmpty()) {
|
|
||||||
textEmptyState.setVisibility(android.view.View.VISIBLE);
|
|
||||||
recyclerView.setVisibility(android.view.View.GONE);
|
|
||||||
} else {
|
|
||||||
textEmptyState.setVisibility(android.view.View.GONE);
|
|
||||||
recyclerView.setVisibility(android.view.View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -96,6 +105,43 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
|||||||
tickerHandler.postDelayed(tickerRunnable, 1000);
|
tickerHandler.postDelayed(tickerRunnable, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applyFilterAndUpdate() {
|
||||||
|
java.util.List<AISVesselEntity> filtered;
|
||||||
|
if (currentQuery == null || currentQuery.isEmpty()) {
|
||||||
|
filtered = new java.util.ArrayList<>(fullList);
|
||||||
|
} else {
|
||||||
|
String q = currentQuery.toLowerCase(java.util.Locale.ROOT);
|
||||||
|
filtered = new java.util.ArrayList<>();
|
||||||
|
for (AISVesselEntity e : fullList) {
|
||||||
|
if (matchesQuery(e, q)) {
|
||||||
|
filtered.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
adapter.submitList(filtered);
|
||||||
|
int targetCount = filtered.size();
|
||||||
|
textTargetCount.setText("AIS цели: " + targetCount);
|
||||||
|
if (targetCount == 0) {
|
||||||
|
textEmptyState.setVisibility(android.view.View.VISIBLE);
|
||||||
|
recyclerView.setVisibility(android.view.View.GONE);
|
||||||
|
} else {
|
||||||
|
textEmptyState.setVisibility(android.view.View.GONE);
|
||||||
|
recyclerView.setVisibility(android.view.View.VISIBLE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesQuery(AISVesselEntity e, String q) {
|
||||||
|
if (e == null) return false;
|
||||||
|
// MMSI (строго строкой), имя, позывной, пункт назначения, IMO, тип судна
|
||||||
|
if (e.mmsi != null && e.mmsi.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
|
||||||
|
if (e.vesselName != null && e.vesselName.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
|
||||||
|
if (e.callSign != null && e.callSign.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
|
||||||
|
if (e.destination != null && e.destination.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
|
||||||
|
if (e.vesselType != null && e.vesselType.toLowerCase(java.util.Locale.ROOT).contains(q)) return true;
|
||||||
|
if (String.valueOf(e.imo).contains(q)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private void loadOurVesselData() {
|
private void loadOurVesselData() {
|
||||||
repository.getLatestOwnVesselAsync(new Repository.RepositoryCallback<VesselEntity>() {
|
repository.getLatestOwnVesselAsync(new Repository.RepositoryCallback<VesselEntity>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
private ImageButton btnCenterOnVessel;
|
private ImageButton btnCenterOnVessel;
|
||||||
private ImageButton btnMapOrientation;
|
private ImageButton btnMapOrientation;
|
||||||
|
private ImageButton btnCursorToggle;
|
||||||
private ImageButton btnSettings;
|
private ImageButton btnSettings;
|
||||||
private ImageButton btnAisTargets;
|
private ImageButton btnAisTargets;
|
||||||
private LinearLayout controlPanel;
|
private LinearLayout controlPanel;
|
||||||
@@ -88,6 +89,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private Runnable compassUpdateRunnable;
|
private Runnable compassUpdateRunnable;
|
||||||
private Runnable coordinatesUpdateRunnable;
|
private Runnable coordinatesUpdateRunnable;
|
||||||
private Runnable compassButtonRotationRunnable;
|
private Runnable compassButtonRotationRunnable;
|
||||||
|
private static final long COMPASS_ANIM_DURATION_MS = 150;
|
||||||
private Vessel lastCompassVessel;
|
private Vessel lastCompassVessel;
|
||||||
private Vessel lastCoordinatesVessel;
|
private Vessel lastCoordinatesVessel;
|
||||||
private static final long UI_UPDATE_THROTTLE_MS = 200; // 5 FPS максимум
|
private static final long UI_UPDATE_THROTTLE_MS = 200; // 5 FPS максимум
|
||||||
@@ -160,6 +162,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
mapView = findViewById(R.id.map_view);
|
mapView = findViewById(R.id.map_view);
|
||||||
btnCenterOnVessel = findViewById(R.id.btn_center_vessel);
|
btnCenterOnVessel = findViewById(R.id.btn_center_vessel);
|
||||||
btnMapOrientation = findViewById(R.id.btn_map_orientation);
|
btnMapOrientation = findViewById(R.id.btn_map_orientation);
|
||||||
|
btnCursorToggle = findViewById(R.id.btn_cursor_toggle);
|
||||||
btnSettings = findViewById(R.id.btn_settings);
|
btnSettings = findViewById(R.id.btn_settings);
|
||||||
btnAisTargets = findViewById(R.id.btn_ais_targets);
|
btnAisTargets = findViewById(R.id.btn_ais_targets);
|
||||||
controlPanel = findViewById(R.id.control_panel);
|
controlPanel = findViewById(R.id.control_panel);
|
||||||
@@ -178,18 +181,36 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
coordinatesWidget.updateVessel(lastCoordinatesVessel);
|
coordinatesWidget.updateVessel(lastCoordinatesVessel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Периодическое обновление поворота кнопки компаса по bearing карты
|
// Периодическое обновление поворота кнопок (компас и ownship)
|
||||||
compassButtonRotationRunnable = () -> {
|
compassButtonRotationRunnable = () -> {
|
||||||
try {
|
try {
|
||||||
if (btnMapOrientation != null && mapController.getCurrentMapInterface() != null) {
|
MapInterface mapIf = mapController != null ? mapController.getCurrentMapInterface() : null;
|
||||||
float bearing = mapController.getCurrentMapInterface().getBearing();
|
if (mapIf != null) {
|
||||||
// Иконка должна указывать север: вращаем противоположно bearing карты
|
float mapBearing = 0f;
|
||||||
btnMapOrientation.setRotation(-bearing);
|
try {
|
||||||
|
mapBearing = mapIf.getBearing();
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
|
||||||
|
// Кнопка компаса должна указывать на север (противоположно bearing карты)
|
||||||
|
float compassTarget = -mapBearing;
|
||||||
|
applySmoothRotation(btnMapOrientation, compassTarget);
|
||||||
|
|
||||||
|
// Кнопка корабля указывает курс относительно карты: course - mapBearing
|
||||||
|
if (btnCenterOnVessel != null && appCoordinator != null) {
|
||||||
|
Vessel own = appCoordinator.getOwnVessel();
|
||||||
|
if (own != null) {
|
||||||
|
double course = own.getCourse();
|
||||||
|
if (!Double.isNaN(course)) {
|
||||||
|
float shipTarget = (float)(course - mapBearing);
|
||||||
|
applySmoothRotation(btnCenterOnVessel, shipTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ignore) {}
|
} catch (Exception ignore) {}
|
||||||
// Планируем следующее обновление
|
// Планируем следующее обновление
|
||||||
if (uiThrottleHandler != null) {
|
if (uiThrottleHandler != null) {
|
||||||
uiThrottleHandler.postDelayed(compassButtonRotationRunnable, UI_UPDATE_THROTTLE_MS);
|
uiThrottleHandler.postDelayed(compassButtonRotationRunnable, Math.max(100, UI_UPDATE_THROTTLE_MS));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tvGpsAge = findViewById(R.id.tv_gps_age);
|
tvGpsAge = findViewById(R.id.tv_gps_age);
|
||||||
@@ -211,6 +232,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private void setupButtonListeners() {
|
private void setupButtonListeners() {
|
||||||
if (btnCenterOnVessel != null) btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
|
if (btnCenterOnVessel != null) btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
|
||||||
if (btnMapOrientation != null) btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
|
if (btnMapOrientation != null) btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
|
||||||
|
if (btnCursorToggle != null) btnCursorToggle.setOnClickListener(v -> toggleCursor());
|
||||||
|
if (btnCursorToggle != null) btnCursorToggle.setOnLongClickListener(v -> { toggleCursor(); return true; });
|
||||||
if (btnSettings != null) btnSettings.setOnClickListener(v -> showSettings());
|
if (btnSettings != null) btnSettings.setOnClickListener(v -> showSettings());
|
||||||
if (btnAisTargets != null) btnAisTargets.setOnClickListener(v -> openAisTargets());
|
if (btnAisTargets != null) btnAisTargets.setOnClickListener(v -> openAisTargets());
|
||||||
|
|
||||||
@@ -297,13 +320,32 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
compassView.post(() -> {
|
compassView.post(() -> {
|
||||||
updateControlPanelPosition();
|
updateControlPanelPosition();
|
||||||
});
|
});
|
||||||
// Стартуем обновление поворота кнопки компаса
|
// Стартуем цикл обновления поворотов кнопок
|
||||||
if (uiThrottleHandler != null) {
|
startCompassButtonsLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCompassButtonsLoop() {
|
||||||
|
if (uiThrottleHandler != null && compassButtonRotationRunnable != null) {
|
||||||
uiThrottleHandler.removeCallbacks(compassButtonRotationRunnable);
|
uiThrottleHandler.removeCallbacks(compassButtonRotationRunnable);
|
||||||
uiThrottleHandler.post(compassButtonRotationRunnable);
|
uiThrottleHandler.post(compassButtonRotationRunnable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applySmoothRotation(ImageButton button, float targetRotation) {
|
||||||
|
if (button == null) return;
|
||||||
|
try {
|
||||||
|
float current = button.getRotation();
|
||||||
|
float normalizedTarget = targetRotation % 360f;
|
||||||
|
if (normalizedTarget < -180f) normalizedTarget += 360f;
|
||||||
|
if (normalizedTarget > 180f) normalizedTarget -= 360f;
|
||||||
|
float delta = normalizedTarget - current;
|
||||||
|
if (delta > 180f) delta -= 360f;
|
||||||
|
if (delta < -180f) delta += 360f;
|
||||||
|
float finalTarget = current + delta;
|
||||||
|
button.animate().rotation(finalTarget).setDuration(COMPASS_ANIM_DURATION_MS).setInterpolator(new android.view.animation.DecelerateInterpolator()).start();
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
private void setupCoordinatesWidget() {
|
private void setupCoordinatesWidget() {
|
||||||
// Настраиваем слушатель изменения размера dock-виджета
|
// Настраиваем слушатель изменения размера dock-виджета
|
||||||
coordinatesWidget.setOnDockResizeListener(newHeight -> {
|
coordinatesWidget.setOnDockResizeListener(newHeight -> {
|
||||||
@@ -322,30 +364,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
updateControlPanelPosition();
|
updateControlPanelPosition();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Устанавливаем виджет координат в dock-режим внизу экрана
|
// Устанавливаем виджет координат в dock-режим внизу экрана без тестовых данных
|
||||||
coordinatesWidget.post(() -> {
|
coordinatesWidget.post(() -> {
|
||||||
Log.d(TAG, "Setting coordinates widget to dock mode");
|
Log.d(TAG, "Setting coordinates widget to dock mode");
|
||||||
coordinatesWidget.setDocked(true, false, 0, 0); // false = dock снизу
|
coordinatesWidget.setDocked(true, false, 0, 0); // false = dock снизу
|
||||||
coordinatesWidget.invalidate(); // Принудительная отрисовка
|
coordinatesWidget.invalidate(); // Принудительная отрисовка
|
||||||
|
|
||||||
// Принудительно обновляем виджет с тестовыми данными (в фоне)
|
|
||||||
android.os.Handler bgHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
|
||||||
bgHandler.post(() -> {
|
|
||||||
try {
|
|
||||||
Vessel testVessel = new Vessel();
|
|
||||||
testVessel.setLatitude(55.7558);
|
|
||||||
testVessel.setLongitude(37.6176);
|
|
||||||
testVessel.setSpeed(5.5);
|
|
||||||
testVessel.setCourse(45.0);
|
|
||||||
testVessel.setAccuracy(3.0f);
|
|
||||||
coordinatesWidget.updateVessel(testVessel);
|
|
||||||
|
|
||||||
// Используем throttled версию
|
|
||||||
updateControlPanelPositionThrottled();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "Ошибка при инициализации тестового виджета: " + e.getMessage(), e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -905,6 +928,31 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Переключает отображение курсора на карте и сохраняет состояние
|
||||||
|
*/
|
||||||
|
private void toggleCursor() {
|
||||||
|
if (mapController == null || mapController.getCurrentMapInterface() == null || settingsManager == null) return;
|
||||||
|
boolean enabled = settingsManager.isCursorEnabled();
|
||||||
|
boolean newState = !enabled;
|
||||||
|
settingsManager.setCursorEnabled(newState);
|
||||||
|
try {
|
||||||
|
if (newState) {
|
||||||
|
mapController.getCurrentMapInterface().showCursor();
|
||||||
|
mapController.getCurrentMapInterface().updateCursorFromMapCenter();
|
||||||
|
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||||
|
((MapLibreMapImpl) mapController.getCurrentMapInterface()).forceCheckAisVesselUnderCursor();
|
||||||
|
}
|
||||||
|
Toast.makeText(this, "Курсор включён", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
mapController.getCurrentMapInterface().hideCursor();
|
||||||
|
Toast.makeText(this, "Курсор выключен", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.w(TAG, "toggleCursor error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновляет позицию панели управления с throttling
|
* Обновляет позицию панели управления с throttling
|
||||||
*/
|
*/
|
||||||
@@ -1081,23 +1129,26 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// Обрабатываем возможный интент центрирования
|
// Обрабатываем возможный интент центрирования
|
||||||
handleCenterIntentIfAny(getIntent());
|
handleCenterIntentIfAny(getIntent());
|
||||||
|
|
||||||
// Восстанавливаем курсор после возврата в активность
|
// Восстанавливаем курсор после возврата в активность согласно настройке
|
||||||
if (mapController.getCurrentMapInterface() != null) {
|
if (mapController.getCurrentMapInterface() != null) {
|
||||||
boolean cursorEnabled = settingsManager.isCursorEnabled();
|
boolean cursorEnabled = settingsManager.isCursorEnabled();
|
||||||
if (cursorEnabled) {
|
if (cursorEnabled) {
|
||||||
mapController.getCurrentMapInterface().showCursor();
|
mapController.getCurrentMapInterface().showCursor();
|
||||||
// Обновляем координаты курсора с центра карты
|
|
||||||
mapController.getCurrentMapInterface().updateCursorFromMapCenter();
|
mapController.getCurrentMapInterface().updateCursorFromMapCenter();
|
||||||
|
|
||||||
// Принудительно проверяем AIS судно под курсором для восстановления панели
|
|
||||||
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||||
((MapLibreMapImpl) mapController.getCurrentMapInterface()).forceCheckAisVesselUnderCursor();
|
((MapLibreMapImpl) mapController.getCurrentMapInterface()).forceCheckAisVesselUnderCursor();
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Явно скрываем при возвращении, если выключен в настройках
|
||||||
|
mapController.getCurrentMapInterface().hideCursor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем разрешения и запускаем контроллеры
|
// Проверяем разрешения и запускаем контроллеры
|
||||||
checkPermissions();
|
checkPermissions();
|
||||||
|
|
||||||
|
// Перезапускаем цикл поворота кнопок после возврата в активити
|
||||||
|
startCompassButtonsLoop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1148,6 +1199,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// Очищаем информацию о AIS судне при паузе активности
|
// Очищаем информацию о AIS судне при паузе активности
|
||||||
if (mapController.getCurrentMapInterface() != null) {
|
if (mapController.getCurrentMapInterface() != null) {
|
||||||
mapController.getCurrentMapInterface().clearAisVesselInfo();
|
mapController.getCurrentMapInterface().clearAisVesselInfo();
|
||||||
|
// Принудительно удаляем overlay курсора, чтобы не накапливался между активити
|
||||||
|
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||||
|
((MapLibreMapImpl) mapController.getCurrentMapInterface()).removeCursorOverlay();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1324,6 +1379,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// Применяем настройки курсора
|
// Применяем настройки курсора
|
||||||
applyCursorSettings(cursorEnabled);
|
applyCursorSettings(cursorEnabled);
|
||||||
|
// Применяем дебаг-режим на карте (красный квадрат)
|
||||||
|
boolean debugEnabled = data.getBooleanExtra("debug_enabled", settingsManager.isDebugEnabled());
|
||||||
|
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||||
|
((MapLibreMapImpl) mapController.getCurrentMapInterface()).setDebugMode(debugEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
if (needsRestart) {
|
if (needsRestart) {
|
||||||
Log.i(TAG, "Требуется перезапуск сервисов");
|
Log.i(TAG, "Требуется перезапуск сервисов");
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
private SwitchMaterial switchVibrationEnabled;
|
private SwitchMaterial switchVibrationEnabled;
|
||||||
private SwitchMaterial switchSoundEnabled;
|
private SwitchMaterial switchSoundEnabled;
|
||||||
private SwitchMaterial switchKeepScreenOn;
|
private SwitchMaterial switchKeepScreenOn;
|
||||||
private SwitchMaterial switchCursorEnabled;
|
private SwitchMaterial switchDebugEnabled;
|
||||||
private Button btnCancel;
|
private Button btnCancel;
|
||||||
private Button btnSave;
|
private Button btnSave;
|
||||||
private Button btnClearPath;
|
private Button btnClearPath;
|
||||||
@@ -104,7 +104,7 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
switchVibrationEnabled = findViewById(R.id.switch_vibration_enabled);
|
switchVibrationEnabled = findViewById(R.id.switch_vibration_enabled);
|
||||||
switchSoundEnabled = findViewById(R.id.switch_sound_enabled);
|
switchSoundEnabled = findViewById(R.id.switch_sound_enabled);
|
||||||
switchKeepScreenOn = findViewById(R.id.switch_keep_screen_on);
|
switchKeepScreenOn = findViewById(R.id.switch_keep_screen_on);
|
||||||
switchCursorEnabled = findViewById(R.id.switch_cursor_enabled);
|
switchDebugEnabled = findViewById(R.id.switch_debug_enabled);
|
||||||
btnCancel = findViewById(R.id.btn_cancel);
|
btnCancel = findViewById(R.id.btn_cancel);
|
||||||
btnSave = findViewById(R.id.btn_save);
|
btnSave = findViewById(R.id.btn_save);
|
||||||
btnClearPath = findViewById(R.id.btn_clear_path);
|
btnClearPath = findViewById(R.id.btn_clear_path);
|
||||||
@@ -154,8 +154,8 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
// Настройки экрана
|
// Настройки экрана
|
||||||
switchKeepScreenOn.setChecked(settingsManager.isKeepScreenOnEnabled());
|
switchKeepScreenOn.setChecked(settingsManager.isKeepScreenOnEnabled());
|
||||||
|
|
||||||
// Настройки курсора
|
// Дебаг
|
||||||
switchCursorEnabled.setChecked(settingsManager.isCursorEnabled());
|
switchDebugEnabled.setChecked(settingsManager.isDebugEnabled());
|
||||||
|
|
||||||
// Путь и предсказание
|
// Путь и предсказание
|
||||||
etPathMaxPoints.setText(String.valueOf(settingsManager.getPathMaxPoints()));
|
etPathMaxPoints.setText(String.valueOf(settingsManager.getPathMaxPoints()));
|
||||||
@@ -182,7 +182,6 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
originalVibrationEnabled = settingsManager.isVibrationEnabled();
|
originalVibrationEnabled = settingsManager.isVibrationEnabled();
|
||||||
originalSoundEnabled = settingsManager.isSoundEnabled();
|
originalSoundEnabled = settingsManager.isSoundEnabled();
|
||||||
originalKeepScreenOnEnabled = settingsManager.isKeepScreenOnEnabled();
|
originalKeepScreenOnEnabled = settingsManager.isKeepScreenOnEnabled();
|
||||||
originalCursorEnabled = settingsManager.isCursorEnabled();
|
|
||||||
|
|
||||||
Log.i(TAG, "Оригинальные настройки сохранены");
|
Log.i(TAG, "Оригинальные настройки сохранены");
|
||||||
}
|
}
|
||||||
@@ -314,7 +313,8 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
settingsManager.setVibrationEnabled(switchVibrationEnabled.isChecked());
|
settingsManager.setVibrationEnabled(switchVibrationEnabled.isChecked());
|
||||||
settingsManager.setSoundEnabled(switchSoundEnabled.isChecked());
|
settingsManager.setSoundEnabled(switchSoundEnabled.isChecked());
|
||||||
settingsManager.setKeepScreenOnEnabled(switchKeepScreenOn.isChecked());
|
settingsManager.setKeepScreenOnEnabled(switchKeepScreenOn.isChecked());
|
||||||
settingsManager.setCursorEnabled(switchCursorEnabled.isChecked());
|
boolean debugEnabled = switchDebugEnabled.isChecked();
|
||||||
|
settingsManager.setDebugEnabled(debugEnabled);
|
||||||
|
|
||||||
// Путь и предсказание
|
// Путь и предсказание
|
||||||
try { settingsManager.setPathMaxPoints(Integer.parseInt(etPathMaxPoints.getText().toString().trim())); } catch (Exception ignored) {}
|
try { settingsManager.setPathMaxPoints(Integer.parseInt(etPathMaxPoints.getText().toString().trim())); } catch (Exception ignored) {}
|
||||||
@@ -338,7 +338,8 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
resultIntent.putExtra("android_nmea_enabled", switchAndroidNMEAEnabled.isChecked());
|
resultIntent.putExtra("android_nmea_enabled", switchAndroidNMEAEnabled.isChecked());
|
||||||
resultIntent.putExtra("udp_nmea_enabled", switchUDPNMEAEnabled.isChecked());
|
resultIntent.putExtra("udp_nmea_enabled", switchUDPNMEAEnabled.isChecked());
|
||||||
resultIntent.putExtra("data_mode", dataMode);
|
resultIntent.putExtra("data_mode", dataMode);
|
||||||
resultIntent.putExtra("cursor_enabled", switchCursorEnabled.isChecked());
|
resultIntent.putExtra("cursor_enabled", settingsManager.isCursorEnabled());
|
||||||
|
resultIntent.putExtra("debug_enabled", debugEnabled);
|
||||||
|
|
||||||
setResult(RESULT_OK, resultIntent);
|
setResult(RESULT_OK, resultIntent);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.grigowashere.aismap.utils.SettingsManager;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -42,7 +43,7 @@ public class AppCoordinator implements
|
|||||||
|
|
||||||
// Состояние приложения
|
// Состояние приложения
|
||||||
private Vessel ownVessel;
|
private Vessel ownVessel;
|
||||||
private List<AISVessel> aisVessels;
|
private Map<String, AISVessel> aisVessels;
|
||||||
private Map<String, VesselPathController> aisPathControllers;
|
private Map<String, VesselPathController> aisPathControllers;
|
||||||
private SettingsManager settingsManager;
|
private SettingsManager settingsManager;
|
||||||
private VesselPathController pathController;
|
private VesselPathController pathController;
|
||||||
@@ -72,7 +73,7 @@ public class AppCoordinator implements
|
|||||||
public AppCoordinator(Context context) {
|
public AppCoordinator(Context context) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.ownVessel = new Vessel();
|
this.ownVessel = new Vessel();
|
||||||
this.aisVessels = new ArrayList<>();
|
this.aisVessels = new LinkedHashMap<>();
|
||||||
this.aisPathControllers = new HashMap<>();
|
this.aisPathControllers = new HashMap<>();
|
||||||
this.settingsManager = new SettingsManager(context);
|
this.settingsManager = new SettingsManager(context);
|
||||||
this.pathController = new VesselPathController(context, settingsManager);
|
this.pathController = new VesselPathController(context, settingsManager);
|
||||||
@@ -268,20 +269,16 @@ public class AppCoordinator implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем дополнительные данные
|
// Обновляем дополнительные данные (без фильтров по нулю, чтобы корректно обрабатывать сбросы)
|
||||||
if (vessel.getCourse() > 0) {
|
|
||||||
ownVessel.setCourse(vessel.getCourse());
|
ownVessel.setCourse(vessel.getCourse());
|
||||||
updateCompass();
|
|
||||||
}
|
|
||||||
if (vessel.getSpeed() > 0) {
|
|
||||||
ownVessel.setSpeed(vessel.getSpeed());
|
ownVessel.setSpeed(vessel.getSpeed());
|
||||||
}
|
|
||||||
if (vessel.getSatellites() > 0) {
|
|
||||||
ownVessel.setSatellites(vessel.getSatellites());
|
ownVessel.setSatellites(vessel.getSatellites());
|
||||||
}
|
ownVessel.setActiveSatellites(vessel.getActiveSatellites());
|
||||||
if (vessel.getAltitude() != 0) {
|
|
||||||
ownVessel.setAltitude(vessel.getAltitude());
|
ownVessel.setAltitude(vessel.getAltitude());
|
||||||
}
|
ownVessel.setFixQuality(vessel.getFixQuality());
|
||||||
|
|
||||||
|
// Обновляем компас после изменения курса
|
||||||
|
updateCompass();
|
||||||
|
|
||||||
// Сохраняем в БД
|
// Сохраняем в БД
|
||||||
dataController.saveVesselPosition(ownVessel);
|
dataController.saveVesselPosition(ownVessel);
|
||||||
@@ -343,8 +340,10 @@ public class AppCoordinator implements
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Добавляем новое судно
|
// Добавляем новое судно
|
||||||
|
if (vessel.getMmsi() != null) {
|
||||||
synchronized (aisVessels) {
|
synchronized (aisVessels) {
|
||||||
aisVessels.add(vessel);
|
aisVessels.put(vessel.getMmsi(), vessel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Если это новое судно сразу пришло с safety-сообщением — уведомим
|
// Если это новое судно сразу пришло с safety-сообщением — уведомим
|
||||||
@@ -404,7 +403,11 @@ public class AppCoordinator implements
|
|||||||
if (aisVessels != null) {
|
if (aisVessels != null) {
|
||||||
synchronized (this.aisVessels) {
|
synchronized (this.aisVessels) {
|
||||||
this.aisVessels.clear();
|
this.aisVessels.clear();
|
||||||
this.aisVessels.addAll(aisVessels);
|
for (AISVessel ais : aisVessels) {
|
||||||
|
if (ais.getMmsi() != null) {
|
||||||
|
this.aisVessels.put(ais.getMmsi(), ais);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,7 +513,7 @@ public class AppCoordinator implements
|
|||||||
if (aisVessels != null && !aisVessels.isEmpty()) {
|
if (aisVessels != null && !aisVessels.isEmpty()) {
|
||||||
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
|
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
|
||||||
synchronized (aisVessels) {
|
synchronized (aisVessels) {
|
||||||
for (AISVessel vessel : aisVessels) {
|
for (AISVessel vessel : aisVessels.values()) {
|
||||||
uiDataNotifier.onAISVesselChanged(vessel);
|
uiDataNotifier.onAISVesselChanged(vessel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,21 +525,19 @@ public class AppCoordinator implements
|
|||||||
// Вспомогательные методы
|
// Вспомогательные методы
|
||||||
|
|
||||||
private AISVessel findAISVesselByMMSI(String mmsi) {
|
private AISVessel findAISVesselByMMSI(String mmsi) {
|
||||||
synchronized (aisVessels) {
|
if (mmsi == null) {
|
||||||
for (AISVessel vessel : aisVessels) {
|
|
||||||
if (mmsi.equals(vessel.getMmsi())) {
|
|
||||||
return vessel;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
synchronized (aisVessels) {
|
||||||
|
return aisVessels.get(mmsi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<AISVessel> getNearbyVessels() {
|
private List<AISVessel> getNearbyVessels() {
|
||||||
List<AISVessel> nearby = new ArrayList<>();
|
List<AISVessel> nearby = new ArrayList<>();
|
||||||
double maxDistance = 10000; // 10 км в метрах
|
double maxDistance = 10000; // 10 км в метрах
|
||||||
|
|
||||||
for (AISVessel vessel : aisVessels) {
|
for (AISVessel vessel : aisVessels.values()) {
|
||||||
double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel);
|
double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel);
|
||||||
if (distance <= maxDistance) {
|
if (distance <= maxDistance) {
|
||||||
nearby.add(vessel);
|
nearby.add(vessel);
|
||||||
@@ -631,7 +632,7 @@ public class AppCoordinator implements
|
|||||||
|
|
||||||
public List<AISVessel> getAISVessels() {
|
public List<AISVessel> getAISVessels() {
|
||||||
synchronized (aisVessels) {
|
synchronized (aisVessels) {
|
||||||
return new ArrayList<>(aisVessels);
|
return new ArrayList<>(aisVessels.values());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import com.grigowashere.aismap.utils.LogSender;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.time.Clock;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Контроллер для парсинга NMEA сообщений
|
* Контроллер для парсинга NMEA сообщений
|
||||||
@@ -174,11 +179,98 @@ public class NMEAParser {
|
|||||||
*/
|
*/
|
||||||
private String getField(String[] fields, int index) {
|
private String getField(String[] fields, int index) {
|
||||||
if (index < fields.length && !fields[index].trim().isEmpty()) {
|
if (index < fields.length && !fields[index].trim().isEmpty()) {
|
||||||
return fields[index].trim();
|
return sanitizeField(fields[index].trim());
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет суффикс контрольной суммы (*xx) из значения поля
|
||||||
|
*/
|
||||||
|
private String sanitizeField(String value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
int starPos = value.indexOf('*');
|
||||||
|
if (starPos >= 0) {
|
||||||
|
return value.substring(0, starPos);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует строку UTC времени hhmmss[.ss] в epoch millis для текущей UTC даты
|
||||||
|
*/
|
||||||
|
private long utcTimeToEpochMillis(String utcTimeStr) {
|
||||||
|
if (utcTimeStr == null || utcTimeStr.length() < 6) return 0L;
|
||||||
|
try {
|
||||||
|
String hh = utcTimeStr.substring(0, 2);
|
||||||
|
String mm = utcTimeStr.substring(2, 4);
|
||||||
|
String ss = utcTimeStr.substring(4); // может содержать дробную часть
|
||||||
|
|
||||||
|
int hours = Integer.parseInt(hh);
|
||||||
|
int minutes = Integer.parseInt(mm);
|
||||||
|
|
||||||
|
int wholeSeconds;
|
||||||
|
int nanoAdjustment;
|
||||||
|
if (ss.contains(".")) {
|
||||||
|
String[] parts = ss.split("\\.", 2);
|
||||||
|
wholeSeconds = Integer.parseInt(parts[0]);
|
||||||
|
// дробную часть переводим в наносекунды (до 9 знаков)
|
||||||
|
String frac = parts[1];
|
||||||
|
if (frac.length() > 9) frac = frac.substring(0, 9);
|
||||||
|
// правым дополнением до 9 знаков
|
||||||
|
while (frac.length() < 9) frac += "0";
|
||||||
|
nanoAdjustment = Integer.parseInt(frac);
|
||||||
|
} else {
|
||||||
|
wholeSeconds = Integer.parseInt(ss);
|
||||||
|
nanoAdjustment = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalTime time = LocalTime.of(hours, minutes, wholeSeconds, nanoAdjustment);
|
||||||
|
// Без даты epoch возвращать нельзя — оставляем 0, чтобы не писать некорректные значения
|
||||||
|
return 0L;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует дату (DDMMYY) и время (HHMMSS[.SS]) UTC в epoch millis
|
||||||
|
*/
|
||||||
|
private long utcDateTimeToEpochMillis(String dateDDMMYY, String timeHHMMSS) {
|
||||||
|
if (dateDDMMYY == null || dateDDMMYY.length() != 6 || timeHHMMSS == null || timeHHMMSS.length() < 6) return 0L;
|
||||||
|
try {
|
||||||
|
int day = Integer.parseInt(dateDDMMYY.substring(0, 2));
|
||||||
|
int month = Integer.parseInt(dateDDMMYY.substring(2, 4));
|
||||||
|
int year = 2000 + Integer.parseInt(dateDDMMYY.substring(4, 6));
|
||||||
|
|
||||||
|
String hh = timeHHMMSS.substring(0, 2);
|
||||||
|
String mm = timeHHMMSS.substring(2, 4);
|
||||||
|
String ss = timeHHMMSS.substring(4);
|
||||||
|
int hours = Integer.parseInt(hh);
|
||||||
|
int minutes = Integer.parseInt(mm);
|
||||||
|
int wholeSeconds;
|
||||||
|
int nanoAdjustment;
|
||||||
|
if (ss.contains(".")) {
|
||||||
|
String[] parts = ss.split("\\.", 2);
|
||||||
|
wholeSeconds = Integer.parseInt(parts[0]);
|
||||||
|
String frac = parts[1];
|
||||||
|
if (frac.length() > 9) frac = frac.substring(0, 9);
|
||||||
|
while (frac.length() < 9) frac += "0";
|
||||||
|
nanoAdjustment = Integer.parseInt(frac);
|
||||||
|
} else {
|
||||||
|
wholeSeconds = Integer.parseInt(ss);
|
||||||
|
nanoAdjustment = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalDate date = LocalDate.of(year, month, day);
|
||||||
|
LocalTime time = LocalTime.of(hours, minutes, wholeSeconds, nanoAdjustment);
|
||||||
|
ZonedDateTime zdt = ZonedDateTime.of(date, time, ZoneOffset.UTC);
|
||||||
|
return zdt.toInstant().toEpochMilli();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Безопасно парсит double значение из поля
|
* Безопасно парсит double значение из поля
|
||||||
*/
|
*/
|
||||||
@@ -311,7 +403,7 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ownVessel.setSatellites(satellites);
|
ownVessel.setActiveSatellites(satellites);
|
||||||
ownVessel.setAltitude(altitude);
|
ownVessel.setAltitude(altitude);
|
||||||
|
|
||||||
// Синхронизируем с GPSLocationListener для получения активных спутников
|
// Синхронизируем с GPSLocationListener для получения активных спутников
|
||||||
@@ -343,6 +435,16 @@ public class NMEAParser {
|
|||||||
// Поле 8: курс в градусах
|
// Поле 8: курс в градусах
|
||||||
double course = parseDoubleField(fields, 8, 0.0);
|
double course = parseDoubleField(fields, 8, 0.0);
|
||||||
|
|
||||||
|
// Устанавливаем время фикса только если есть и дата, и время
|
||||||
|
String timeStr = getField(fields, 1); // HHMMSS.SS
|
||||||
|
String dateStr = getField(fields, 9); // DDMMYY
|
||||||
|
if (timeStr != null && timeStr.length() >= 6) {
|
||||||
|
ownVessel.setFixTimeNmea(timeStr);
|
||||||
|
}
|
||||||
|
long epochMillis = utcDateTimeToEpochMillis(dateStr, timeStr);
|
||||||
|
if (epochMillis > 0) {
|
||||||
|
ownVessel.setFixTime(epochMillis);
|
||||||
|
}
|
||||||
// Убираем шумный лог RMC данных
|
// Убираем шумный лог RMC данных
|
||||||
|
|
||||||
// В гибридном режиме не обновляем координаты
|
// В гибридном режиме не обновляем координаты
|
||||||
@@ -414,18 +516,45 @@ public class NMEAParser {
|
|||||||
* Формат: $GPGLL,lat,N/S,lon,E/W,time,status,mode*checksum
|
* Формат: $GPGLL,lat,N/S,lon,E/W,time,status,mode*checksum
|
||||||
*/
|
*/
|
||||||
private void parseGLL(String[] fields) {
|
private void parseGLL(String[] fields) {
|
||||||
if (hybridMode) {
|
// Разбираем время фикса (поле 5), статус (поле 6) и режим (поле 7)
|
||||||
// Убираем шумный лог игнорирования GLL
|
String utcTimeStr = getField(fields, 5); // hhmmss.ss
|
||||||
return;
|
String status = getField(fields, 6); // A/V
|
||||||
|
String mode = getField(fields, 7); // A/D/E/M/S/N (может отсутствовать)
|
||||||
|
|
||||||
|
// Устанавливаем fixQuality на основе статуса и режима
|
||||||
|
if (status != null) {
|
||||||
|
if ("A".equals(status)) {
|
||||||
|
// Валидные данные: уточняем по mode
|
||||||
|
if (mode != null) {
|
||||||
|
switch (mode) {
|
||||||
|
case "A": ownVessel.setFixQuality("AUTONOMOUS"); break;
|
||||||
|
case "D": ownVessel.setFixQuality("DIFFERENTIAL"); break;
|
||||||
|
case "E": ownVessel.setFixQuality("ESTIMATED"); break;
|
||||||
|
case "M": ownVessel.setFixQuality("MANUAL"); break;
|
||||||
|
case "S": ownVessel.setFixQuality("SIMULATOR"); break;
|
||||||
|
case "N": ownVessel.setFixQuality("NOT_VALID"); break;
|
||||||
|
default: ownVessel.setFixQuality("AUTONOMOUS"); break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ownVessel.setFixQuality("AUTONOMOUS");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ownVessel.setFixQuality("NOT_VALID");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Убираем шумный лог парсинга GLL
|
// GLL не содержит дату — epoch не пишем, но строковое время сохраним
|
||||||
|
if (utcTimeStr != null && utcTimeStr.length() >= 6) {
|
||||||
|
ownVessel.setFixTimeNmea(utcTimeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если не в гибридном режиме — обновляем координаты
|
||||||
|
if (!hybridMode) {
|
||||||
// Поля 1,2: широта и направление
|
// Поля 1,2: широта и направление
|
||||||
String latStr = getField(fields, 1);
|
String latStr = getField(fields, 1);
|
||||||
String latDir = getField(fields, 2);
|
String latDir = getField(fields, 2);
|
||||||
if (latStr != null && latDir != null) {
|
if (latStr != null && latDir != null) {
|
||||||
double latitude = parseCoordinate(latStr, latDir.equals("N"));
|
double latitude = parseCoordinate(latStr, "N".equals(latDir));
|
||||||
ownVessel.setLatitude(latitude);
|
ownVessel.setLatitude(latitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -433,11 +562,10 @@ public class NMEAParser {
|
|||||||
String lonStr = getField(fields, 3);
|
String lonStr = getField(fields, 3);
|
||||||
String lonDir = getField(fields, 4);
|
String lonDir = getField(fields, 4);
|
||||||
if (lonStr != null && lonDir != null) {
|
if (lonStr != null && lonDir != null) {
|
||||||
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
|
double longitude = parseCoordinate(lonStr, "E".equals(lonDir));
|
||||||
ownVessel.setLongitude(longitude);
|
ownVessel.setLongitude(longitude);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Убираем шумный лог GLL координат
|
|
||||||
|
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onVesselUpdated(ownVessel);
|
listener.onVesselUpdated(ownVessel);
|
||||||
@@ -470,7 +598,7 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Убираем шумный лог GSV спутников
|
// Убираем шумный лог GSV спутников
|
||||||
|
int totalSatellites1 = 0;
|
||||||
// Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
|
// Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
|
||||||
for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
|
for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
|
||||||
if (i + 3 < fields.length) {
|
if (i + 3 < fields.length) {
|
||||||
@@ -478,7 +606,7 @@ public class NMEAParser {
|
|||||||
String elevation = getField(fields, i + 1);
|
String elevation = getField(fields, i + 1);
|
||||||
String azimuth = getField(fields, i + 2);
|
String azimuth = getField(fields, i + 2);
|
||||||
String snr = getField(fields, i + 3);
|
String snr = getField(fields, i + 3);
|
||||||
|
totalSatellites1++;
|
||||||
if (satId != null) {
|
if (satId != null) {
|
||||||
Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
|
Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
|
||||||
satId, elevation, azimuth, snr));
|
satId, elevation, azimuth, snr));
|
||||||
@@ -507,7 +635,7 @@ public class NMEAParser {
|
|||||||
|
|
||||||
// Обновляем общее количество спутников
|
// Обновляем общее количество спутников
|
||||||
int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites;
|
int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites;
|
||||||
ownVessel.setSatellites(totalSatellites);
|
ownVessel.setSatellites(totalSatellites1);
|
||||||
|
|
||||||
// Синхронизируем с GPSLocationListener для получения активных спутников
|
// Синхронизируем с GPSLocationListener для получения активных спутников
|
||||||
if (gpsLocationListener != null) {
|
if (gpsLocationListener != null) {
|
||||||
@@ -557,7 +685,7 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ownVessel.setSatellites(satellites);
|
ownVessel.setActiveSatellites(satellites);
|
||||||
ownVessel.setAltitude(altitude);
|
ownVessel.setAltitude(altitude);
|
||||||
|
|
||||||
// Синхронизируем с GPSLocationListener для получения активных спутников
|
// Синхронизируем с GPSLocationListener для получения активных спутников
|
||||||
@@ -592,6 +720,14 @@ public class NMEAParser {
|
|||||||
|
|
||||||
Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d",
|
Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d",
|
||||||
year, month, day, timeStr, timezoneHours, timezoneMinutes));
|
year, month, day, timeStr, timezoneHours, timezoneMinutes));
|
||||||
|
// Если и дата, и время валидны — выставим epoch fixTime
|
||||||
|
if (year > 0 && month > 0 && day > 0 && timeStr != null && timeStr.length() >= 6) {
|
||||||
|
String dateStr = String.format(java.util.Locale.US, "%02d%02d%02d", day, month, (year % 100));
|
||||||
|
long epochMillis = utcDateTimeToEpochMillis(dateStr, timeStr);
|
||||||
|
if (epochMillis > 0) {
|
||||||
|
ownVessel.setFixTime(epochMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем время последнего обновления
|
// Обновляем время последнего обновления
|
||||||
ownVessel.setLastUpdate(java.time.LocalDateTime.now());
|
ownVessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
@@ -613,6 +749,17 @@ public class NMEAParser {
|
|||||||
private void parseGSA(String[] fields) {
|
private void parseGSA(String[] fields) {
|
||||||
// Убираем шумный лог парсинга GSA
|
// Убираем шумный лог парсинга GSA
|
||||||
|
|
||||||
|
// Mode1 (авто/ручной) и тип фикса (1/2/3)
|
||||||
|
String mode1 = getField(fields, 1); // A/M
|
||||||
|
int fixType = parseIntField(fields, 2, 1); // 1=no fix, 2=2D, 3=3D
|
||||||
|
String fixQuality = "NO_FIX";
|
||||||
|
if (fixType == 2) {
|
||||||
|
fixQuality = ("A".equals(mode1) ? "AUTO_2D" : ("M".equals(mode1) ? "MANUAL_2D" : "2D"));
|
||||||
|
} else if (fixType == 3) {
|
||||||
|
fixQuality = ("A".equals(mode1) ? "AUTO_3D" : ("M".equals(mode1) ? "MANUAL_3D" : "3D"));
|
||||||
|
}
|
||||||
|
ownVessel.setFixQuality(fixQuality);
|
||||||
|
|
||||||
// Подсчитываем активные спутники (поля 3-14 содержат ID спутников)
|
// Подсчитываем активные спутники (поля 3-14 содержат ID спутников)
|
||||||
int activeSatellites = 0;
|
int activeSatellites = 0;
|
||||||
for (int i = 3; i <= 14 && i < fields.length; i++) {
|
for (int i = 3; i <= 14 && i < fields.length; i++) {
|
||||||
@@ -623,6 +770,7 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
|
// Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
|
||||||
double pdop = 0.0;
|
double pdop = 0.0;
|
||||||
double hdop = 0.0;
|
double hdop = 0.0;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import com.grigowashere.aismap.controllers.AppCoordinator;
|
|||||||
import com.grigowashere.aismap.view.CursorOverlay;
|
import com.grigowashere.aismap.view.CursorOverlay;
|
||||||
import com.grigowashere.aismap.R;
|
import com.grigowashere.aismap.R;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
@@ -193,16 +195,41 @@ public class MapLibreMapImpl implements MapInterface {
|
|||||||
// VesselPathController будет установлен через setVesselPathController()
|
// VesselPathController будет установлен через setVesselPathController()
|
||||||
this.pathController = null;
|
this.pathController = null;
|
||||||
this.cursorOverlay = new CursorOverlay(context);
|
this.cursorOverlay = new CursorOverlay(context);
|
||||||
|
// Инициализируем флаг отладки из настроек
|
||||||
|
this.debugMode = settingsManager.isDebugEnabled();
|
||||||
|
|
||||||
// Добавляем overlay курсора в MapView
|
// Добавляем overlay курсора НАД картой: в родителя MapView, чтобы он был поверх GL слоя
|
||||||
if (mapView instanceof ViewGroup) {
|
ViewParent mapParent = mapView.getParent();
|
||||||
ViewGroup parent = (ViewGroup) mapView;
|
if (mapParent instanceof ViewGroup) {
|
||||||
// Проверяем, не добавлен ли уже курсор
|
ViewGroup parent = (ViewGroup) mapParent;
|
||||||
if (parent.findViewById(R.id.cursor_cross) == null) {
|
// Удаляем все прежние cursor_overlay из родителя (на случай повторной инициализации)
|
||||||
parent.addView(cursorOverlay.getView());
|
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
|
||||||
|
View child = parent.getChildAt(i);
|
||||||
|
Object tag = child.getTag();
|
||||||
|
if (tag != null && "cursor_overlay".equals(tag)) {
|
||||||
|
parent.removeViewAt(i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
View v = cursorOverlay.getView();
|
||||||
|
if (v.getParent() != parent) {
|
||||||
|
if (v.getParent() instanceof ViewGroup) {
|
||||||
|
((ViewGroup) v.getParent()).removeView(v);
|
||||||
|
}
|
||||||
|
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
);
|
||||||
|
// Вставляем поверх MapView
|
||||||
|
int mapIndex = parent.indexOfChild(mapView);
|
||||||
|
int insertIndex = Math.min(mapIndex + 1, parent.getChildCount());
|
||||||
|
parent.addView(v, insertIndex, lp);
|
||||||
|
}
|
||||||
|
// Поднимаем на передний план
|
||||||
|
v.bringToFront();
|
||||||
|
v.setTranslationZ(2f);
|
||||||
|
}
|
||||||
|
|
||||||
// Инициализируем throttling для карты
|
// Инициализируем throttling для карты
|
||||||
setupMapThrottling();
|
setupMapThrottling();
|
||||||
}
|
}
|
||||||
@@ -288,7 +315,19 @@ public class MapLibreMapImpl implements MapInterface {
|
|||||||
if (maplibreMap != null) {
|
if (maplibreMap != null) {
|
||||||
maplibreMap.removeOnMapClickListener(onMapClickListener);
|
maplibreMap.removeOnMapClickListener(onMapClickListener);
|
||||||
}
|
}
|
||||||
|
// Удаляем overlay курсора из родителя, чтобы не накапливался между активити
|
||||||
if (mapView != null) {
|
if (mapView != null) {
|
||||||
|
ViewParent mapParent = mapView.getParent();
|
||||||
|
if (mapParent instanceof ViewGroup) {
|
||||||
|
ViewGroup parent = (ViewGroup) mapParent;
|
||||||
|
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
|
||||||
|
View child = parent.getChildAt(i);
|
||||||
|
Object tag = child.getTag();
|
||||||
|
if (tag != null && "cursor_overlay".equals(tag)) {
|
||||||
|
parent.removeViewAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
mapView.onStop();
|
mapView.onStop();
|
||||||
}
|
}
|
||||||
staleHandler.removeCallbacks(staleRunnable);
|
staleHandler.removeCallbacks(staleRunnable);
|
||||||
@@ -1062,30 +1101,28 @@ public class MapLibreMapImpl implements MapInterface {
|
|||||||
Log.d(TAG, "appendOwnPathPoint: skipping zero coordinates");
|
Log.d(TAG, "appendOwnPathPoint: skipping zero coordinates");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ВРЕМЕННО ОТКЛЮЧАЕМ ПРОВЕРКУ РАССТОЯНИЯ ДЛЯ ТЕСТИРОВАНИЯ
|
|
||||||
// Проверяем, изменились ли координаты (строгая проверка на дублирование)
|
// Проверяем, изменились ли координаты (строгая проверка на дублирование)
|
||||||
// if (ownPathCoords.length() > 0) {
|
if (ownPathCoords.length() > 0) {
|
||||||
// JSONArray lastPoint = ownPathCoords.getJSONArray(ownPathCoords.length() - 1);
|
JSONArray lastPoint = ownPathCoords.getJSONArray(ownPathCoords.length() - 1);
|
||||||
// double lastLon = lastPoint.getDouble(0);
|
double lastLon = lastPoint.getDouble(0);
|
||||||
// double lastLat = lastPoint.getDouble(1);
|
double lastLat = lastPoint.getDouble(1);
|
||||||
//
|
|
||||||
// // Строгая проверка на точное совпадение координат
|
|
||||||
// if (lon == lastLon && lat == lastLat) {
|
|
||||||
// Log.d(TAG, "appendOwnPathPoint: exact duplicate coordinates, skipping");
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// double distance = Math.sqrt(Math.pow(lon - lastLon, 2) + Math.pow(lat - lastLat, 2));
|
|
||||||
// Log.d(TAG, "appendOwnPathPoint: distance=" + distance + " (threshold=0.00001)");
|
|
||||||
// if (distance < 0.00001) { // примерно 1 метр
|
|
||||||
// Log.d(TAG, "appendOwnPathPoint: vessel not moving, distance=" + distance);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ЗАХАРДКОДИМ МАКСИМАЛЬНОЕ КОЛИЧЕСТВО ТОЧЕК ДЛЯ ТЕСТИРОВАНИЯ
|
// Строгая проверка на точное совпадение координат
|
||||||
int max = 5000; // settingsManager.getPathMaxPoints();
|
if (lon == lastLon && lat == lastLat) {
|
||||||
|
Log.d(TAG, "appendOwnPathPoint: exact duplicate coordinates, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double distance = Math.sqrt(Math.pow(lon - lastLon, 2) + Math.pow(lat - lastLat, 2));
|
||||||
|
Log.d(TAG, "appendOwnPathPoint: distance=" + distance + " (threshold=0.00001)");
|
||||||
|
if (distance < 0.00001) { // примерно 1 метр
|
||||||
|
Log.d(TAG, "appendOwnPathPoint: vessel not moving, distance=" + distance);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Лимит точек пути из настроек
|
||||||
|
int max = settingsManager.getPathMaxPoints();
|
||||||
if (ownPathCoords.length() >= max) {
|
if (ownPathCoords.length() >= max) {
|
||||||
// удаляем из начала
|
// удаляем из начала
|
||||||
ownPathCoords.remove(0);
|
ownPathCoords.remove(0);
|
||||||
@@ -1940,15 +1977,42 @@ public class MapLibreMapImpl implements MapInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showCursor() {
|
public void showCursor() {
|
||||||
if (cursorOverlay != null) {
|
try {
|
||||||
cursorOverlay.showCursor();
|
if (cursorOverlay == null) {
|
||||||
|
cursorOverlay = new com.grigowashere.aismap.view.CursorOverlay(context);
|
||||||
}
|
}
|
||||||
|
// Убедимся, что overlay добавлен в иерархию и находится поверх карты
|
||||||
|
if (mapView instanceof android.view.ViewGroup) {
|
||||||
|
android.view.ViewGroup parent = (android.view.ViewGroup) mapView;
|
||||||
|
android.view.View v = cursorOverlay.getView();
|
||||||
|
if (v.getParent() != parent) {
|
||||||
|
if (v.getParent() instanceof android.view.ViewGroup) {
|
||||||
|
((android.view.ViewGroup) v.getParent()).removeView(v);
|
||||||
|
}
|
||||||
|
android.view.ViewGroup.LayoutParams lp = new android.view.ViewGroup.LayoutParams(
|
||||||
|
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
);
|
||||||
|
parent.addView(v, lp);
|
||||||
|
}
|
||||||
|
v.bringToFront();
|
||||||
|
v.setTranslationZ(1f);
|
||||||
|
}
|
||||||
|
cursorOverlay.showCursor();
|
||||||
|
// Обновим размеры, координаты и попробуем восстановить панель AIS
|
||||||
|
updateScreenDimensions();
|
||||||
|
updateCursorFromMapCenter();
|
||||||
|
// Подпишемся на движение камеры (на случай если слушатель потерян)
|
||||||
|
setupMapMovementListener();
|
||||||
|
forceCheckAisVesselUnderCursor();
|
||||||
|
} catch (Exception ignore) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void hideCursor() {
|
public void hideCursor() {
|
||||||
if (cursorOverlay != null) {
|
if (cursorOverlay != null) {
|
||||||
cursorOverlay.hideCursor();
|
cursorOverlay.hideCursor();
|
||||||
|
clearAisVesselInfo();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1961,25 +2025,64 @@ public class MapLibreMapImpl implements MapInterface {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateCursorFromMapCenter() {
|
public void updateCursorFromMapCenter() {
|
||||||
if (cursorOverlay != null && maplibreMap != null && mapView != null) {
|
if (cursorOverlay != null && mapView != null) {
|
||||||
try {
|
try {
|
||||||
// Получаем координаты центра карты
|
// Получаем координаты центра карты (если карта уже готова)
|
||||||
org.maplibre.android.geometry.LatLng center = maplibreMap.getCameraPosition().target;
|
org.maplibre.android.geometry.LatLng center = maplibreMap != null ? maplibreMap.getCameraPosition().target : null;
|
||||||
Log.d(TAG, String.format("updateCursorFromMapCenter: center=%.6f,%.6f",
|
Log.d(TAG, String.format("updateCursorFromMapCenter: center=%.6f,%.6f",
|
||||||
center.getLatitude(), center.getLongitude()));
|
center != null ? center.getLatitude() : 0,
|
||||||
|
center != null ? center.getLongitude() : 0));
|
||||||
|
|
||||||
|
if (center != null) {
|
||||||
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
|
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
|
||||||
|
|
||||||
// Проверяем, есть ли AIS судно под курсором
|
// Проверяем, есть ли AIS судно под курсором
|
||||||
checkAisVesselUnderCursor(center);
|
checkAisVesselUnderCursor(center);
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.w(TAG, "updateCursorFromMapCenter: MapView may be destroyed: " + e.getMessage());
|
Log.w(TAG, "updateCursorFromMapCenter: MapView may be destroyed: " + e.getMessage());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "updateCursorFromMapCenter: cursorOverlay, maplibreMap или mapView равны null");
|
Log.d(TAG, "updateCursorFromMapCenter: cursorOverlay или mapView равны null");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Публичный метод для принудительного удаления cursor overlay из вью-иерархии
|
||||||
|
* Используется активностью в onPause/onStop, чтобы исключить наслоение при возврате
|
||||||
|
*/
|
||||||
|
public void removeCursorOverlay() {
|
||||||
|
try {
|
||||||
|
if (cursorOverlay != null) {
|
||||||
|
cursorOverlay.hideCursor();
|
||||||
|
}
|
||||||
|
if (mapView != null) {
|
||||||
|
// Удаляем из родителя MapView
|
||||||
|
ViewParent mapParent = mapView.getParent();
|
||||||
|
if (mapParent instanceof ViewGroup) {
|
||||||
|
ViewGroup parent = (ViewGroup) mapParent;
|
||||||
|
for (int i = parent.getChildCount() - 1; i >= 0; i--) {
|
||||||
|
View child = parent.getChildAt(i);
|
||||||
|
Object tag = child.getTag();
|
||||||
|
if (tag != null && "cursor_overlay".equals(tag)) {
|
||||||
|
parent.removeViewAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// На всякий случай: уберём из самого MapView (если когда-то добавлялся туда)
|
||||||
|
if (mapView instanceof ViewGroup) {
|
||||||
|
ViewGroup mv = (ViewGroup) mapView;
|
||||||
|
for (int i = mv.getChildCount() - 1; i >= 0; i--) {
|
||||||
|
View child = mv.getChildAt(i);
|
||||||
|
Object tag = child.getTag();
|
||||||
|
if (tag != null && "cursor_overlay".equals(tag)) {
|
||||||
|
mv.removeViewAt(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, есть ли AIS судно под курсором (в центре экрана)
|
* Проверяет, есть ли AIS судно под курсором (в центре экрана)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class Vessel {
|
|||||||
private float accuracy; // точность в метрах
|
private float accuracy; // точность в метрах
|
||||||
private long fixTime; // время последнего фикса
|
private long fixTime; // время последнего фикса
|
||||||
private String fixQuality; // качество фикса (GPS, DGPS, RTK и т.д.)
|
private String fixQuality; // качество фикса (GPS, DGPS, RTK и т.д.)
|
||||||
|
private String fixTimeNmea; // время фикса как пришло в NMEA (строка HHMMSS[.SS])
|
||||||
|
|
||||||
public Vessel() {
|
public Vessel() {
|
||||||
this.lastUpdate = LocalDateTime.now();
|
this.lastUpdate = LocalDateTime.now();
|
||||||
@@ -104,6 +105,9 @@ public class Vessel {
|
|||||||
public String getFixQuality() { return fixQuality; }
|
public String getFixQuality() { return fixQuality; }
|
||||||
public void setFixQuality(String fixQuality) { this.fixQuality = fixQuality; }
|
public void setFixQuality(String fixQuality) { this.fixQuality = fixQuality; }
|
||||||
|
|
||||||
|
public String getFixTimeNmea() { return fixTimeNmea; }
|
||||||
|
public void setFixTimeNmea(String fixTimeNmea) { this.fixTimeNmea = fixTimeNmea; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновляет данные судна
|
* Обновляет данные судна
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -147,7 +147,22 @@ public class BottomSheetsManager {
|
|||||||
if (tvGPSQuality != null) tvGPSQuality.setText((vessel.getGPSQualityDescription()!=null) ? String.format("📊 Качество GPS: %s", vessel.getGPSQualityDescription()) : "📊 Качество GPS: --");
|
if (tvGPSQuality != null) tvGPSQuality.setText((vessel.getGPSQualityDescription()!=null) ? String.format("📊 Качество GPS: %s", vessel.getGPSQualityDescription()) : "📊 Качество GPS: --");
|
||||||
if (tvSatellites != null) tvSatellites.setText((vessel.getSatellites()>0) ? String.format("Спутники: %d/%d", vessel.getActiveSatellites(), vessel.getSatellites()) : "Спутники: --/--");
|
if (tvSatellites != null) tvSatellites.setText((vessel.getSatellites()>0) ? String.format("Спутники: %d/%d", vessel.getActiveSatellites(), vessel.getSatellites()) : "Спутники: --/--");
|
||||||
if (tvDOP != null) tvDOP.setText((vessel.getPdop()>0) ? String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", vessel.getPdop(), vessel.getHdop(), vessel.getVdop()) : "📈 DOP: PDOP=-- HDOP=-- VDOP=--");
|
if (tvDOP != null) tvDOP.setText((vessel.getPdop()>0) ? String.format("📈 DOP: PDOP=%.2f HDOP=%.2f VDOP=%.2f", vessel.getPdop(), vessel.getHdop(), vessel.getVdop()) : "📈 DOP: PDOP=-- HDOP=-- VDOP=--");
|
||||||
if (tvFixTime != null) tvFixTime.setText((vessel.getFixTime()>0) ? String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(new java.util.Date(vessel.getFixTime()))) : "🕐 Время фикса: --");
|
if (tvFixTime != null) {
|
||||||
|
String t = vessel.getFixTimeNmea();
|
||||||
|
if (t != null && !t.isEmpty()) {
|
||||||
|
String pretty = t;
|
||||||
|
int dot = pretty.indexOf('.');
|
||||||
|
if (dot > 0) pretty = pretty.substring(0, dot);
|
||||||
|
if (pretty.length() == 6) {
|
||||||
|
pretty = pretty.substring(0,2) + ":" + pretty.substring(2,4) + ":" + pretty.substring(4,6);
|
||||||
|
}
|
||||||
|
tvFixTime.setText(String.format("🕐 Время фикса: %s", pretty));
|
||||||
|
} else if (vessel.getFixTime() > 0) {
|
||||||
|
tvFixTime.setText(String.format("🕐 Время фикса: %s", new java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()).format(new java.util.Date(vessel.getFixTime()))));
|
||||||
|
} else {
|
||||||
|
tvFixTime.setText("🕐 Время фикса: --");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (tvFixQuality != null) tvFixQuality.setText((vessel.getFixQuality()!=null) ? String.format("🔒 Качество фикса: %s", vessel.getFixQuality()) : "🔒 Качество фикса: --");
|
if (tvFixQuality != null) tvFixQuality.setText((vessel.getFixQuality()!=null) ? String.format("🔒 Качество фикса: %s", vessel.getFixQuality()) : "🔒 Качество фикса: --");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ public class SettingsManager {
|
|||||||
private static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled";
|
private static final String KEY_KEEP_SCREEN_ON_ENABLED = "keep_screen_on_enabled";
|
||||||
private static final String KEY_CURSOR_ENABLED = "cursor_enabled";
|
private static final String KEY_CURSOR_ENABLED = "cursor_enabled";
|
||||||
private static final String KEY_NOTIFICATIONS_ENABLED = "notifications_enabled";
|
private static final String KEY_NOTIFICATIONS_ENABLED = "notifications_enabled";
|
||||||
|
private static final String KEY_DEBUG_ENABLED = "debug_enabled";
|
||||||
private static final String KEY_ANDROID_GPS_ENABLED = "android_gps_enabled";
|
private static final String KEY_ANDROID_GPS_ENABLED = "android_gps_enabled";
|
||||||
|
|
||||||
// Значения по умолчанию
|
// Значения по умолчанию
|
||||||
@@ -56,6 +57,7 @@ public class SettingsManager {
|
|||||||
private static final boolean DEFAULT_CURSOR_ENABLED = false;
|
private static final boolean DEFAULT_CURSOR_ENABLED = false;
|
||||||
private static final boolean DEFAULT_NOTIFICATIONS_ENABLED = true;
|
private static final boolean DEFAULT_NOTIFICATIONS_ENABLED = true;
|
||||||
private static final boolean DEFAULT_ANDROID_GPS_ENABLED = true;
|
private static final boolean DEFAULT_ANDROID_GPS_ENABLED = true;
|
||||||
|
private static final boolean DEFAULT_DEBUG_ENABLED = false;
|
||||||
|
|
||||||
// Режимы работы с данными
|
// Режимы работы с данными
|
||||||
public static final String DATA_MODE_HYBRID = "hybrid";
|
public static final String DATA_MODE_HYBRID = "hybrid";
|
||||||
@@ -469,4 +471,19 @@ public class SettingsManager {
|
|||||||
Log.i(TAG, "Уведомления: " + (enabled ? "включены" : "выключены"));
|
Log.i(TAG, "Уведомления: " + (enabled ? "включены" : "выключены"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включен ли дебаг-режим
|
||||||
|
*/
|
||||||
|
public boolean isDebugEnabled() {
|
||||||
|
return prefs.getBoolean(KEY_DEBUG_ENABLED, DEFAULT_DEBUG_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает дебаг-режим
|
||||||
|
*/
|
||||||
|
public void setDebugEnabled(boolean enabled) {
|
||||||
|
prefs.edit().putBoolean(KEY_DEBUG_ENABLED, enabled).apply();
|
||||||
|
Log.i(TAG, "Дебаг-режим: " + (enabled ? "включен" : "выключен"));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,13 +114,13 @@ public class CompassView extends BaseDockWidget {
|
|||||||
float baseHeight = dp(80); // базовая высота
|
float baseHeight = dp(80); // базовая высота
|
||||||
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
|
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
|
||||||
|
|
||||||
// Простой текст для проверки
|
// Простой текст для проверки (убрана надпись "КОМПАС")
|
||||||
paint.setColor(Color.WHITE);
|
paint.setColor(Color.WHITE);
|
||||||
paint.setTextSize(24 * scaleFactor);
|
paint.setTextSize(24 * scaleFactor);
|
||||||
paint.setTextAlign(Paint.Align.CENTER);
|
paint.setTextAlign(Paint.Align.CENTER);
|
||||||
canvas.drawText("КОМПАС", w/2, h/2, paint);
|
float topTextY = dp(18) * scaleFactor;
|
||||||
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
|
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, topTextY, paint);
|
||||||
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
|
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, topTextY + 24 * scaleFactor, paint);
|
||||||
|
|
||||||
// Плавное обновление азимута
|
// Плавное обновление азимута
|
||||||
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
||||||
@@ -157,7 +157,10 @@ public class CompassView extends BaseDockWidget {
|
|||||||
if (degree % 45 == 0) {
|
if (degree % 45 == 0) {
|
||||||
int directionIndex = (degree / 45) % 8;
|
int directionIndex = (degree / 45) % 8;
|
||||||
if (directionIndex < directions.length) {
|
if (directionIndex < directions.length) {
|
||||||
paint.setTextSize(18 * scaleFactor);
|
// Буква стороны света увеличивается при приближении к центру
|
||||||
|
float proximity = 1f - Math.min(Math.abs(relativeDegree) / (visibleDegrees / 2f), 1f);
|
||||||
|
float letterSize = (24f + 36f * proximity) * scaleFactor; // 24..48
|
||||||
|
paint.setTextSize(letterSize);
|
||||||
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
|
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ public class CursorOverlay {
|
|||||||
private void initializeViews() {
|
private void initializeViews() {
|
||||||
LayoutInflater inflater = LayoutInflater.from(context);
|
LayoutInflater inflater = LayoutInflater.from(context);
|
||||||
overlayView = inflater.inflate(R.layout.cursor, null);
|
overlayView = inflater.inflate(R.layout.cursor, null);
|
||||||
|
// Помечаем overlay для последующего обнаружения/удаления
|
||||||
|
overlayView.setTag("cursor_overlay");
|
||||||
|
|
||||||
tvCursorLatitude = overlayView.findViewById(R.id.tv_cursor_latitude);
|
tvCursorLatitude = overlayView.findViewById(R.id.tv_cursor_latitude);
|
||||||
tvCursorLongitude = overlayView.findViewById(R.id.tv_cursor_longitude);
|
tvCursorLongitude = overlayView.findViewById(R.id.tv_cursor_longitude);
|
||||||
@@ -350,6 +352,10 @@ public class CursorOverlay {
|
|||||||
public void hideCursor() {
|
public void hideCursor() {
|
||||||
if (overlayView != null) {
|
if (overlayView != null) {
|
||||||
overlayView.setVisibility(View.GONE);
|
overlayView.setVisibility(View.GONE);
|
||||||
|
// Также скрываем панели, чтобы исключить ложное наложение
|
||||||
|
if (aisVesselInfoPanel != null) aisVesselInfoPanel.setVisibility(View.GONE);
|
||||||
|
if (coordinatesPanel != null) coordinatesPanel.clearAnimation();
|
||||||
|
if (distanceBearingPanel != null) distanceBearingPanel.clearAnimation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,6 +365,18 @@ public class CursorOverlay {
|
|||||||
public void showCursor() {
|
public void showCursor() {
|
||||||
if (overlayView != null) {
|
if (overlayView != null) {
|
||||||
overlayView.setVisibility(View.VISIBLE);
|
overlayView.setVisibility(View.VISIBLE);
|
||||||
|
// Поднимем overlay над картой
|
||||||
|
overlayView.bringToFront();
|
||||||
|
overlayView.setElevation(10f);
|
||||||
|
// Гарантируем, что крест выше, а панель AIS ещё выше
|
||||||
|
View cursorCross = overlayView.findViewById(R.id.cursor_cross);
|
||||||
|
if (cursorCross != null) {
|
||||||
|
cursorCross.setElevation(12f);
|
||||||
|
}
|
||||||
|
if (aisVesselInfoPanel != null) {
|
||||||
|
aisVesselInfoPanel.bringToFront();
|
||||||
|
aisVesselInfoPanel.setElevation(20f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,15 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<EditText
|
||||||
|
android:id="@+id/edit_search"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="Поиск по MMSI, названию..."
|
||||||
|
android:inputType="text"
|
||||||
|
android:imeOptions="actionDone"
|
||||||
|
android:padding="12dp"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/text_target_count"
|
android:id="@+id/text_target_count"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -48,6 +48,17 @@
|
|||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_cursor_toggle"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:background="@drawable/button_background"
|
||||||
|
android:src="@drawable/cursorcross"
|
||||||
|
android:contentDescription="Курсор"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/btn_settings"
|
android:id="@+id/btn_settings"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
|
|||||||
@@ -545,7 +545,7 @@
|
|||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
<!-- Настройки курсора -->
|
<!-- Дебаг-режим -->
|
||||||
<com.google.android.material.card.MaterialCardView
|
<com.google.android.material.card.MaterialCardView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -562,7 +562,7 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="🎯 Курсор на карте"
|
android:text="🐞 Режим отладки"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"
|
||||||
android:textColor="@android:color/black"
|
android:textColor="@android:color/black"
|
||||||
@@ -571,29 +571,20 @@
|
|||||||
<TextView
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Настройте отображение курсора с координатами центра экрана:"
|
android:text="Включите расширенное логирование и диагностические элементы UI."
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="@android:color/darker_gray"
|
android:textColor="@android:color/darker_gray"
|
||||||
android:layout_marginBottom="16dp" />
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
<com.google.android.material.switchmaterial.SwitchMaterial
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
android:id="@+id/switch_cursor_enabled"
|
android:id="@+id/switch_debug_enabled"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Показать курсор"
|
android:text="Включить дебаг-режим"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:checked="false"
|
android:checked="false"
|
||||||
android:layout_marginBottom="8dp" />
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Отображать крест в центре экрана с координатами и информацией о расстоянии"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@android:color/darker_gray"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:layout_marginStart="16dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
android:id="@+id/coordinates_panel"
|
android:id="@+id/coordinates_panel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/panel_background"
|
android:background="@android:color/transparent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:visibility="visible">
|
android:visibility="visible">
|
||||||
@@ -29,7 +29,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="Широта: --"
|
android:text="Широта: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_cursor_longitude"
|
android:id="@+id/tv_cursor_longitude"
|
||||||
@@ -38,7 +43,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="Долгота: --"
|
android:text="Долгота: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -47,7 +57,7 @@
|
|||||||
android:id="@+id/distance_bearing_panel"
|
android:id="@+id/distance_bearing_panel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/panel_background"
|
android:background="@android:color/transparent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:visibility="visible">
|
android:visibility="visible">
|
||||||
@@ -60,6 +70,11 @@
|
|||||||
android:text="Rnd:"
|
android:text="Rnd:"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -71,6 +86,11 @@
|
|||||||
android:text="Brg: --"
|
android:text="Brg: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -81,7 +101,7 @@
|
|||||||
android:id="@+id/ais_vessel_info_panel"
|
android:id="@+id/ais_vessel_info_panel"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/panel_background"
|
android:background="@android:color/transparent"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
@@ -93,7 +113,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="MMSI: --"
|
android:text="MMSI: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_ais_name"
|
android:id="@+id/tv_ais_name"
|
||||||
@@ -102,7 +127,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="Название: --"
|
android:text="Название: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_ais_call_sign"
|
android:id="@+id/tv_ais_call_sign"
|
||||||
@@ -111,7 +141,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="Позывной: --"
|
android:text="Позывной: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_ais_cog"
|
android:id="@+id/tv_ais_cog"
|
||||||
@@ -120,7 +155,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="COG: --"
|
android:text="COG: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tv_ais_sog"
|
android:id="@+id/tv_ais_sog"
|
||||||
@@ -129,7 +169,12 @@
|
|||||||
android:fontFamily="monospace"
|
android:fontFamily="monospace"
|
||||||
android:text="SOG: --"
|
android:text="SOG: --"
|
||||||
android:textColor="@android:color/white"
|
android:textColor="@android:color/white"
|
||||||
android:textSize="12sp" />
|
android:textSize="12sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:shadowColor="#000000"
|
||||||
|
android:shadowDx="0"
|
||||||
|
android:shadowDy="0"
|
||||||
|
android:shadowRadius="2" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
```plantuml
|
```plantuml
|
||||||
@startuml AIS_Map_Architecture
|
@startuml AIS_Map_Architecture
|
||||||
|
|
||||||
!theme plain
|
!define RECTANGLE class
|
||||||
skinparam classAttributeIconSize 0
|
skinparam classAttributeIconSize 0
|
||||||
skinparam classFontSize 10
|
skinparam classFontSize 9
|
||||||
skinparam packageFontSize 12
|
skinparam packageFontSize 11
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam classBackgroundColor white
|
||||||
|
skinparam packageBackgroundColor lightblue
|
||||||
|
skinparam packageBorderColor black
|
||||||
|
skinparam classBorderColor black
|
||||||
|
skinparam interfaceBackgroundColor lightgreen
|
||||||
|
skinparam interfaceBorderColor black
|
||||||
|
|
||||||
package "Main Activity" {
|
package "Main Activity" {
|
||||||
class MainActivity {
|
class MainActivity {
|
||||||
@@ -26,8 +33,6 @@ package "Main Activity" {
|
|||||||
+ onResume()
|
+ onResume()
|
||||||
+ onPause()
|
+ onPause()
|
||||||
+ onDestroy()
|
+ onDestroy()
|
||||||
+ onCreateOptionsMenu()
|
|
||||||
+ onOptionsItemSelected()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AisTargetsActivity {
|
class AisTargetsActivity {
|
||||||
|
|||||||
@@ -0,0 +1,796 @@
|
|||||||
|
# Диаграмма классов AIS Map Application (PlantUML - Без Graphviz)
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml AIS_Map_Architecture_NoGraphviz
|
||||||
|
|
||||||
|
!define RECTANGLE class
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
skinparam classFontSize 8
|
||||||
|
skinparam packageFontSize 10
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam classBackgroundColor white
|
||||||
|
skinparam packageBackgroundColor lightblue
|
||||||
|
skinparam packageBorderColor black
|
||||||
|
skinparam classBorderColor black
|
||||||
|
skinparam interfaceBackgroundColor lightgreen
|
||||||
|
skinparam interfaceBorderColor black
|
||||||
|
skinparam arrowColor black
|
||||||
|
skinparam arrowThickness 1
|
||||||
|
|
||||||
|
package "Main Activity" {
|
||||||
|
class MainActivity {
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- MenuBinder menuBinder
|
||||||
|
- BottomSheetsBinder bottomSheetsBinder
|
||||||
|
- PermissionsBinder permissionsBinder
|
||||||
|
- MapController mapController
|
||||||
|
- CompassController compassController
|
||||||
|
- UIRenderingCoordinator uiCoordinator
|
||||||
|
+ onCreate()
|
||||||
|
+ onResume()
|
||||||
|
+ onPause()
|
||||||
|
+ onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AisTargetsActivity {
|
||||||
|
- AisTargetsAdapter adapter
|
||||||
|
- List<AISVessel> aisVessels
|
||||||
|
+ onCreate()
|
||||||
|
+ updateAISList()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsActivity {
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
+ onCreate()
|
||||||
|
+ saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Controllers Factory" {
|
||||||
|
interface ControllersFactory {
|
||||||
|
+ createAppCoordinator() : AppCoordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultControllersFactory {
|
||||||
|
+ createAppCoordinator() : AppCoordinator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Core Controllers" {
|
||||||
|
class AppCoordinator {
|
||||||
|
- Context context
|
||||||
|
- NMEAController nmeaController
|
||||||
|
- NetworkController networkController
|
||||||
|
- DataController dataController
|
||||||
|
- NotificationController notificationController
|
||||||
|
- CompassController compassController
|
||||||
|
- MapController mapController
|
||||||
|
- Vessel ownVessel
|
||||||
|
- List<AISVessel> aisVessels
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
+ initializeControllers()
|
||||||
|
+ startServices()
|
||||||
|
+ stopServices()
|
||||||
|
+ onVesselUpdated()
|
||||||
|
+ onAISVesselUpdated()
|
||||||
|
+ onDOPUpdated()
|
||||||
|
+ onDataReceived()
|
||||||
|
+ onNotificationShown()
|
||||||
|
+ onCompassChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class NMEAController {
|
||||||
|
- Context context
|
||||||
|
- NMEAParser nmeaParser
|
||||||
|
- AndroidNMEAListener androidNmeaListener
|
||||||
|
- GPSLocationListener gpsLocationListener
|
||||||
|
- ExecutorService executor
|
||||||
|
+ startAndroidNMEAListener()
|
||||||
|
+ stopAndroidNMEAListener()
|
||||||
|
+ startGPSLocationListener()
|
||||||
|
+ stopGPSLocationListener()
|
||||||
|
+ parseNMEAData()
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkController {
|
||||||
|
- Context context
|
||||||
|
- UDPListener udpListener
|
||||||
|
- ExecutorService executor
|
||||||
|
- int udpPort
|
||||||
|
- boolean isUDPEnabled
|
||||||
|
+ setUDPEnabled()
|
||||||
|
+ startUDPListener()
|
||||||
|
+ stopUDPListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataController {
|
||||||
|
- Context context
|
||||||
|
- Repository repository
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- ExecutorService executor
|
||||||
|
+ restoreDataAsync()
|
||||||
|
+ saveVesselData()
|
||||||
|
+ saveAISData()
|
||||||
|
+ performDatabaseCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationController {
|
||||||
|
- Context context
|
||||||
|
- NotificationService notificationService
|
||||||
|
+ notifyNewAISTarget()
|
||||||
|
+ notifySafetyMessage()
|
||||||
|
+ notifyGPSStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompassController {
|
||||||
|
- Context context
|
||||||
|
- CompassSensor compassSensor
|
||||||
|
- Handler uiHandler
|
||||||
|
+ startCompass()
|
||||||
|
+ stopCompass()
|
||||||
|
+ isCompassAvailable() : boolean
|
||||||
|
+ isCompassActive() : boolean
|
||||||
|
+ getCompassStatus() : String
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapController {
|
||||||
|
- Context context
|
||||||
|
- MapInterface currentMapInterface
|
||||||
|
- MapView mapView
|
||||||
|
- MapLibreMapView mapLibreView
|
||||||
|
- List<MapInterfaceChangeListener> listeners
|
||||||
|
+ addMapInterfaceChangeListener()
|
||||||
|
+ removeMapInterfaceChangeListener()
|
||||||
|
+ switchToYandexMaps()
|
||||||
|
+ switchToMapLibre()
|
||||||
|
+ getCurrentMapInterface() : MapInterface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "UI Binders" {
|
||||||
|
class MenuBinder {
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- MenuActions actions
|
||||||
|
+ onCreateOptionsMenu()
|
||||||
|
+ onPrepareOptionsMenu()
|
||||||
|
+ onOptionsItemSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomSheetsBinder {
|
||||||
|
- Context context
|
||||||
|
- BottomSheetDialog ownVesselBottomSheet
|
||||||
|
- BottomSheetDialog aisVesselBottomSheet
|
||||||
|
- View ownBottomSheetView
|
||||||
|
- View aisBottomSheetView
|
||||||
|
- AISVessel currentAISVessel
|
||||||
|
+ init()
|
||||||
|
+ initAIS()
|
||||||
|
+ showOwnVesselSheet()
|
||||||
|
+ showAISVesselSheet()
|
||||||
|
+ startAutoUpdate()
|
||||||
|
+ stopAutoUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomSheetsManager {
|
||||||
|
- Context context
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- BottomSheetDialog ownVesselBottomSheet
|
||||||
|
- BottomSheetDialog aisVesselBottomSheet
|
||||||
|
- View bottomSheetView
|
||||||
|
- View aisBottomSheetView
|
||||||
|
- AISVessel currentAISVessel
|
||||||
|
+ init()
|
||||||
|
+ showOwnVesselSheet()
|
||||||
|
+ showAISVesselSheet()
|
||||||
|
+ updateOwnVesselUI()
|
||||||
|
+ updateAISBottomSheetUI()
|
||||||
|
+ stopAutoUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PermissionsBinder {
|
||||||
|
- Activity activity
|
||||||
|
+ ensurePermission() : boolean
|
||||||
|
+ handleOnRequestPermissionsResult() : boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Data Processing" {
|
||||||
|
class NMEAParser {
|
||||||
|
- Vessel ownVessel
|
||||||
|
- List<AISVessel> aisVessels
|
||||||
|
- NMEAParserListener listener
|
||||||
|
- GPSLocationListener gpsLocationListener
|
||||||
|
- Map<String, Map<Integer, String>> aisFragments
|
||||||
|
- boolean hybridMode
|
||||||
|
+ parseNMEA()
|
||||||
|
+ setHybridMode()
|
||||||
|
+ setGPSLocationListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
class UDPListener {
|
||||||
|
- int port
|
||||||
|
- DatagramSocket socket
|
||||||
|
- ExecutorService executor
|
||||||
|
- AtomicBoolean isRunning
|
||||||
|
- UDPListenerCallback callback
|
||||||
|
+ start()
|
||||||
|
+ stop()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidNMEAListener {
|
||||||
|
- LocationManager locationManager
|
||||||
|
- NMEAMessageCallback callback
|
||||||
|
- boolean isListening
|
||||||
|
+ startListening() : boolean
|
||||||
|
+ stopListening()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class GPSLocationListener {
|
||||||
|
- Context context
|
||||||
|
- LocationManager locationManager
|
||||||
|
- LocationCallback callback
|
||||||
|
- boolean isListening
|
||||||
|
- int satelliteCount
|
||||||
|
- int activeSatellites
|
||||||
|
- double pdop
|
||||||
|
- double hdop
|
||||||
|
- double vdop
|
||||||
|
+ startListening() : boolean
|
||||||
|
+ stopListening()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VesselPathController {
|
||||||
|
- Context context
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- SharedPreferences prefs
|
||||||
|
- String vesselId
|
||||||
|
- Handler uiHandler
|
||||||
|
- List<VesselPathPoint> pathPoints
|
||||||
|
- VesselPathPoint lastPoint
|
||||||
|
+ addPathPoint()
|
||||||
|
+ getPathPoints() : List<VesselPathPoint>
|
||||||
|
+ clearPath()
|
||||||
|
+ savePath()
|
||||||
|
+ loadPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Maps" {
|
||||||
|
interface MapInterface {
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
|
||||||
|
class YandexMapImpl {
|
||||||
|
- Context context
|
||||||
|
- MapView mapView
|
||||||
|
- MapObjectCollection mapObjects
|
||||||
|
- MarkerClickListener markerClickListener
|
||||||
|
- YandexMarkerManager markerManager
|
||||||
|
- CursorOverlay cursorOverlay
|
||||||
|
- Vessel ownVessel
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLibreMapImpl {
|
||||||
|
- Context context
|
||||||
|
- MapView mapView
|
||||||
|
- MapLibreMap mapLibreMap
|
||||||
|
- MarkerClickListener markerClickListener
|
||||||
|
- CursorOverlay cursorOverlay
|
||||||
|
- Vessel ownVessel
|
||||||
|
- Map<String, AISVessel> aisVessels
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapForgeImpl {
|
||||||
|
- Context context
|
||||||
|
- MapView mapView
|
||||||
|
- MarkerClickListener markerClickListener
|
||||||
|
- CursorOverlay cursorOverlay
|
||||||
|
- Vessel ownVessel
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Data Models" {
|
||||||
|
class Vessel {
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double magneticCompass
|
||||||
|
- int signalStrength
|
||||||
|
- LocalDateTime lastUpdate
|
||||||
|
- String vesselName
|
||||||
|
- String mmsi
|
||||||
|
- String callSign
|
||||||
|
- double altitude
|
||||||
|
- int satellites
|
||||||
|
- int activeSatellites
|
||||||
|
- double pdop
|
||||||
|
- double hdop
|
||||||
|
- double vdop
|
||||||
|
- float accuracy
|
||||||
|
- long fixTime
|
||||||
|
- String fixQuality
|
||||||
|
+ updatePosition()
|
||||||
|
+ updateGPSQuality()
|
||||||
|
+ getGPSQualityPercentage() : int
|
||||||
|
+ getGPSQualityDescription() : String
|
||||||
|
}
|
||||||
|
|
||||||
|
class AISVessel {
|
||||||
|
- String mmsi
|
||||||
|
- String vesselName
|
||||||
|
- String callSign
|
||||||
|
- int imo
|
||||||
|
- String vesselType
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double rateOfTurn
|
||||||
|
- double length
|
||||||
|
- double width
|
||||||
|
- double draft
|
||||||
|
- String destination
|
||||||
|
- LocalDateTime eta
|
||||||
|
- LocalDateTime lastUpdate
|
||||||
|
- int signalStrength
|
||||||
|
- boolean isActive
|
||||||
|
- String navigationalStatus
|
||||||
|
- String lastSafetyMessage
|
||||||
|
- boolean positionAccuracy
|
||||||
|
- String vesselClass
|
||||||
|
- String vendorId
|
||||||
|
- boolean selected
|
||||||
|
+ updatePosition()
|
||||||
|
+ isDataStale() : boolean
|
||||||
|
+ shouldBeRemoved() : boolean
|
||||||
|
+ getMinutesSinceLastUpdate() : long
|
||||||
|
}
|
||||||
|
|
||||||
|
class VesselPathPoint {
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- long timestamp
|
||||||
|
+ VesselPathPoint()
|
||||||
|
+ toJSON() : String
|
||||||
|
+ fromJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Database" {
|
||||||
|
abstract class AppDatabase {
|
||||||
|
+ aisVesselDao() : AISVesselDao
|
||||||
|
+ vesselDao() : VesselDao
|
||||||
|
+ getInstance() : AppDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
class Repository {
|
||||||
|
- AISVesselDao aisVesselDao
|
||||||
|
- VesselDao vesselDao
|
||||||
|
- ExecutorService ioExecutor
|
||||||
|
+ upsertAIS()
|
||||||
|
+ deleteStaleAIS()
|
||||||
|
+ getAllAISSync() : List<AISVesselEntity>
|
||||||
|
+ observeAllAIS() : LiveData<List<AISVesselEntity>>
|
||||||
|
+ getAISByMmsiSync() : AISVesselEntity
|
||||||
|
+ upsertOwnVessel()
|
||||||
|
+ getLatestOwnVesselSync() : VesselEntity
|
||||||
|
+ getLatestOwnVesselAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AISVesselDao {
|
||||||
|
+ upsert()
|
||||||
|
+ deleteStale()
|
||||||
|
+ getAll() : List<AISVesselEntity>
|
||||||
|
+ observeAll() : LiveData<List<AISVesselEntity>>
|
||||||
|
+ getByMmsi() : AISVesselEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VesselDao {
|
||||||
|
+ upsert()
|
||||||
|
+ getLatest() : VesselEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
class AISVesselEntity {
|
||||||
|
- String mmsi
|
||||||
|
- String vesselName
|
||||||
|
- String callSign
|
||||||
|
- int imo
|
||||||
|
- String vesselType
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double rateOfTurn
|
||||||
|
- double length
|
||||||
|
- double width
|
||||||
|
- double draft
|
||||||
|
- String destination
|
||||||
|
- long etaEpochMs
|
||||||
|
- long lastUpdateEpochMs
|
||||||
|
- int signalStrength
|
||||||
|
- boolean isActive
|
||||||
|
- String navigationalStatus
|
||||||
|
- String lastSafetyMessage
|
||||||
|
- boolean positionAccuracy
|
||||||
|
- String vesselClass
|
||||||
|
- String vendorId
|
||||||
|
}
|
||||||
|
|
||||||
|
class VesselEntity {
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double magneticCompass
|
||||||
|
- int signalStrength
|
||||||
|
- long lastUpdateEpochMs
|
||||||
|
- String vesselName
|
||||||
|
- String mmsi
|
||||||
|
- String callSign
|
||||||
|
- double altitude
|
||||||
|
- int satellites
|
||||||
|
- int activeSatellites
|
||||||
|
- double pdop
|
||||||
|
- double hdop
|
||||||
|
- double vdop
|
||||||
|
- float accuracy
|
||||||
|
- long fixTime
|
||||||
|
- String fixQuality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "UI Components" {
|
||||||
|
class UIRenderingCoordinator {
|
||||||
|
- MapInterface mapInterface
|
||||||
|
- Handler uiHandler
|
||||||
|
- Vessel pendingVesselUpdate
|
||||||
|
- Map<String, AISVessel> pendingAISUpdates
|
||||||
|
- Set<String> pendingAISRemovals
|
||||||
|
- Runnable vesselUpdateRunnable
|
||||||
|
- Runnable aisUpdateRunnable
|
||||||
|
- Runnable pathUpdateRunnable
|
||||||
|
- boolean vesselUpdatePending
|
||||||
|
- boolean aisUpdatePending
|
||||||
|
- boolean pathUpdatePending
|
||||||
|
+ requestVesselUpdate()
|
||||||
|
+ requestAISUpdate()
|
||||||
|
+ requestAISRemoval()
|
||||||
|
+ flushPendingOperations()
|
||||||
|
+ cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UIDataChangeNotifier {
|
||||||
|
+ onVesselPositionChanged()
|
||||||
|
+ onGPSQualityChanged()
|
||||||
|
+ onAISVesselChanged()
|
||||||
|
+ onAISVesselRemoved()
|
||||||
|
+ onVesselPathChanged()
|
||||||
|
+ onRequestCenterMap()
|
||||||
|
+ onCompassUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompassView {
|
||||||
|
- float azimuth
|
||||||
|
- Paint compassPaint
|
||||||
|
- Paint needlePaint
|
||||||
|
- Paint textPaint
|
||||||
|
- List<AISVessel> nearbyVessels
|
||||||
|
+ setAzimuth()
|
||||||
|
+ setNearbyVessels()
|
||||||
|
+ onDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompassSensor {
|
||||||
|
- SensorManager sensorManager
|
||||||
|
- Sensor magnetometer
|
||||||
|
- Sensor accelerometer
|
||||||
|
- CompassListener callback
|
||||||
|
- float[] lastAccelerometer
|
||||||
|
- float[] lastMagnetometer
|
||||||
|
- boolean lastAccelerometerSet
|
||||||
|
- boolean lastMagnetometerSet
|
||||||
|
- float[] rotationMatrix
|
||||||
|
- float[] orientation
|
||||||
|
+ startListening()
|
||||||
|
+ stopListening()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoordinatesDockWidget {
|
||||||
|
- TextView latitudeText
|
||||||
|
- TextView longitudeText
|
||||||
|
- TextView accuracyText
|
||||||
|
- TextView satellitesText
|
||||||
|
- TextView qualityText
|
||||||
|
+ updateCoordinates()
|
||||||
|
+ updateGPSQuality()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CursorOverlay {
|
||||||
|
- ViewGroup parentView
|
||||||
|
- TextView coordinatesText
|
||||||
|
- TextView vesselInfoText
|
||||||
|
- boolean isVisible
|
||||||
|
+ show()
|
||||||
|
+ hide()
|
||||||
|
+ updateCoordinates()
|
||||||
|
+ setVesselInfo()
|
||||||
|
+ clearVesselInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Services" {
|
||||||
|
class NotificationService {
|
||||||
|
- Context context
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- Vibrator vibrator
|
||||||
|
- ToneGenerator toneGenerator
|
||||||
|
- boolean isInitialized
|
||||||
|
+ showSafetyAlert()
|
||||||
|
+ showNewVesselNotification()
|
||||||
|
+ clearNotifications()
|
||||||
|
+ setVibrationEnabled()
|
||||||
|
+ setSoundEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AISForegroundService {
|
||||||
|
- Context context
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- NotificationManager notificationManager
|
||||||
|
- boolean isRunning
|
||||||
|
+ startForeground()
|
||||||
|
+ stopForeground()
|
||||||
|
+ onStartCommand()
|
||||||
|
+ onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Utils" {
|
||||||
|
class SettingsManager {
|
||||||
|
- Context context
|
||||||
|
- SharedPreferences prefs
|
||||||
|
+ getUDPPort() : int
|
||||||
|
+ setUDPPort()
|
||||||
|
+ isUDPEnabled() : boolean
|
||||||
|
+ setUDPEnabled()
|
||||||
|
+ isAndroidNMEAEnabled() : boolean
|
||||||
|
+ setAndroidNMEAEnabled()
|
||||||
|
+ isUDPNMEAEnabled() : boolean
|
||||||
|
+ setUDPNMEAEnabled()
|
||||||
|
+ getDataMode() : String
|
||||||
|
+ setDataMode()
|
||||||
|
+ getDataStaleWarningMinutes() : int
|
||||||
|
+ setDataStaleWarningMinutes()
|
||||||
|
+ getDataStaleRemoveMinutes() : int
|
||||||
|
+ setDataStaleRemoveMinutes()
|
||||||
|
+ isPathTrackingEnabled() : boolean
|
||||||
|
+ setPathTrackingEnabled()
|
||||||
|
+ getPathColor() : int
|
||||||
|
+ setPathColor()
|
||||||
|
+ getPredictionColor() : int
|
||||||
|
+ setPredictionColor()
|
||||||
|
+ getPathWidth() : float
|
||||||
|
+ setPathWidth()
|
||||||
|
+ getPredictionWidth() : float
|
||||||
|
+ setPredictionWidth()
|
||||||
|
+ getPathMaxPoints() : int
|
||||||
|
+ setPathMaxPoints()
|
||||||
|
+ getPredictionHorizonSec() : int
|
||||||
|
+ setPredictionHorizonSec()
|
||||||
|
+ isVibrationEnabled() : boolean
|
||||||
|
+ setVibrationEnabled()
|
||||||
|
+ isSoundEnabled() : boolean
|
||||||
|
+ setSoundEnabled()
|
||||||
|
+ isKeepScreenOnEnabled() : boolean
|
||||||
|
+ setKeepScreenOnEnabled()
|
||||||
|
+ isCursorEnabled() : boolean
|
||||||
|
+ setCursorEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoUtils {
|
||||||
|
+ calculateDistance() : double
|
||||||
|
+ calculateBearing() : double
|
||||||
|
+ isValidCoordinate() : boolean
|
||||||
|
+ formatCoordinate() : String
|
||||||
|
+ convertToDecimalDegrees() : double
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogSender {
|
||||||
|
+ sendLog()
|
||||||
|
+ sendError()
|
||||||
|
+ sendWarning()
|
||||||
|
+ sendInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MIDToCountry {
|
||||||
|
+ getCountryByMID() : String
|
||||||
|
+ getCountryName() : String
|
||||||
|
+ isValidMID() : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavigationUtils {
|
||||||
|
+ calculateCourse() : double
|
||||||
|
+ calculateSpeed() : double
|
||||||
|
+ calculateETA() : LocalDateTime
|
||||||
|
+ isCollisionRisk() : boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Основные связи
|
||||||
|
MainActivity --> AppCoordinator : uses
|
||||||
|
MainActivity --> MenuBinder : uses
|
||||||
|
MainActivity --> BottomSheetsBinder : uses
|
||||||
|
MainActivity --> PermissionsBinder : uses
|
||||||
|
MainActivity --> MapController : uses
|
||||||
|
MainActivity --> CompassController : uses
|
||||||
|
MainActivity --> UIRenderingCoordinator : uses
|
||||||
|
MainActivity --> CompassView : uses
|
||||||
|
MainActivity --> CoordinatesDockWidget : uses
|
||||||
|
MainActivity --> BottomSheetsManager : uses
|
||||||
|
|
||||||
|
ControllersFactory <|.. DefaultControllersFactory : implements
|
||||||
|
MainActivity --> ControllersFactory : uses
|
||||||
|
DefaultControllersFactory --> AppCoordinator : creates
|
||||||
|
|
||||||
|
AppCoordinator --> NMEAController : coordinates
|
||||||
|
AppCoordinator --> NetworkController : coordinates
|
||||||
|
AppCoordinator --> DataController : coordinates
|
||||||
|
AppCoordinator --> NotificationController : coordinates
|
||||||
|
AppCoordinator --> CompassController : coordinates
|
||||||
|
AppCoordinator --> MapController : coordinates
|
||||||
|
AppCoordinator --> VesselPathController : uses
|
||||||
|
AppCoordinator --> SettingsManager : uses
|
||||||
|
AppCoordinator --> UIRenderingCoordinator : uses
|
||||||
|
|
||||||
|
NMEAController --> NMEAParser : uses
|
||||||
|
NMEAController --> AndroidNMEAListener : uses
|
||||||
|
NMEAController --> GPSLocationListener : uses
|
||||||
|
|
||||||
|
NetworkController --> UDPListener : uses
|
||||||
|
|
||||||
|
DataController --> Repository : uses
|
||||||
|
DataController --> SettingsManager : uses
|
||||||
|
|
||||||
|
NotificationController --> NotificationService : uses
|
||||||
|
|
||||||
|
CompassController --> CompassSensor : uses
|
||||||
|
|
||||||
|
MenuBinder --> AppCoordinator : uses
|
||||||
|
MenuBinder --> SettingsManager : uses
|
||||||
|
BottomSheetsBinder --> Context : uses
|
||||||
|
BottomSheetsManager --> AppCoordinator : uses
|
||||||
|
PermissionsBinder --> Activity : uses
|
||||||
|
|
||||||
|
MapController --> MapInterface : manages
|
||||||
|
MapController --> YandexMapImpl : creates
|
||||||
|
MapController --> MapLibreMapImpl : creates
|
||||||
|
MapController --> MapForgeImpl : creates
|
||||||
|
|
||||||
|
YandexMapImpl ..|> MapInterface : implements
|
||||||
|
MapLibreMapImpl ..|> MapInterface : implements
|
||||||
|
MapForgeImpl ..|> MapInterface : implements
|
||||||
|
|
||||||
|
Repository --> AppDatabase : uses
|
||||||
|
Repository --> AISVesselDao : uses
|
||||||
|
Repository --> VesselDao : uses
|
||||||
|
|
||||||
|
AppDatabase --> AISVesselEntity : contains
|
||||||
|
AppDatabase --> VesselEntity : contains
|
||||||
|
|
||||||
|
UIRenderingCoordinator ..|> UIDataChangeNotifier : implements
|
||||||
|
|
||||||
|
NMEAParser --> Vessel : creates/updates
|
||||||
|
NMEAParser --> AISVessel : creates/updates
|
||||||
|
NMEAParser --> GPSLocationListener : uses
|
||||||
|
|
||||||
|
VesselPathController --> VesselPathPoint : manages
|
||||||
|
VesselPathController --> SettingsManager : uses
|
||||||
|
|
||||||
|
NotificationService --> SettingsManager : uses
|
||||||
|
|
||||||
|
CompassSensor --> CompassView : updates
|
||||||
|
CompassView --> AISVessel : displays
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Описание PlantUML диаграммы без Graphviz
|
||||||
|
|
||||||
|
### 🎯 **Ключевые изменения для совместимости:**
|
||||||
|
|
||||||
|
1. **Убрана зависимость от Graphviz** - диаграмма использует только встроенные возможности PlantUML
|
||||||
|
2. **Упрощены настройки** - оставлены только базовые skinparam параметры
|
||||||
|
3. **Оптимизированы размеры** - уменьшены размеры шрифтов для лучшей читаемости
|
||||||
|
4. **Убраны сложные элементы** - удалены некоторые методы для упрощения
|
||||||
|
|
||||||
|
### 📦 **Структура пакетов:**
|
||||||
|
|
||||||
|
- **Main Activity** - основные активности приложения
|
||||||
|
- **Controllers Factory** - фабрика для создания контроллеров
|
||||||
|
- **Core Controllers** - основные контроллеры системы
|
||||||
|
- **UI Binders** - компоненты для управления UI
|
||||||
|
- **Data Processing** - обработка данных и парсинг
|
||||||
|
- **Maps** - система карт и маркеров
|
||||||
|
- **Data Models** - модели данных
|
||||||
|
- **Database** - слой базы данных
|
||||||
|
- **UI Components** - UI компоненты
|
||||||
|
- **Services** - сервисы приложения
|
||||||
|
- **Utils** - утилиты и вспомогательные классы
|
||||||
|
|
||||||
|
### ✅ **Преимущества этой версии:**
|
||||||
|
|
||||||
|
1. **Совместимость** - работает без Graphviz
|
||||||
|
2. **Производительность** - быстрее генерируется
|
||||||
|
3. **Портативность** - работает в большинстве редакторов
|
||||||
|
4. **Читаемость** - четкая структура и связи
|
||||||
|
|
||||||
|
### 🔧 **Как использовать:**
|
||||||
|
|
||||||
|
1. **Онлайн редакторы:** PlantUML Online Server, PlantText
|
||||||
|
2. **IDE плагины:** IntelliJ IDEA, VS Code, Eclipse
|
||||||
|
3. **Командная строка:** PlantUML jar файл
|
||||||
|
4. **Документация:** GitLab, GitHub поддерживают PlantUML
|
||||||
|
|
||||||
|
Эта версия должна работать без проблем и показывать всю архитектуру вашего AIS Map приложения!
|
||||||
@@ -0,0 +1,793 @@
|
|||||||
|
# Диаграмма классов AIS Map Application (PlantUML - Упрощенная версия)
|
||||||
|
|
||||||
|
```plantuml
|
||||||
|
@startuml AIS_Map_Architecture_Simple
|
||||||
|
|
||||||
|
skinparam classAttributeIconSize 0
|
||||||
|
skinparam classFontSize 9
|
||||||
|
skinparam packageFontSize 11
|
||||||
|
skinparam backgroundColor white
|
||||||
|
skinparam classBackgroundColor white
|
||||||
|
skinparam packageBackgroundColor lightblue
|
||||||
|
skinparam packageBorderColor black
|
||||||
|
skinparam classBorderColor black
|
||||||
|
skinparam interfaceBackgroundColor lightgreen
|
||||||
|
skinparam interfaceBorderColor black
|
||||||
|
|
||||||
|
package "Main Activity" {
|
||||||
|
class MainActivity {
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- MenuBinder menuBinder
|
||||||
|
- BottomSheetsBinder bottomSheetsBinder
|
||||||
|
- PermissionsBinder permissionsBinder
|
||||||
|
- MapController mapController
|
||||||
|
- CompassController compassController
|
||||||
|
- UIRenderingCoordinator uiCoordinator
|
||||||
|
+ onCreate()
|
||||||
|
+ onResume()
|
||||||
|
+ onPause()
|
||||||
|
+ onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AisTargetsActivity {
|
||||||
|
- AisTargetsAdapter adapter
|
||||||
|
- List<AISVessel> aisVessels
|
||||||
|
+ onCreate()
|
||||||
|
+ updateAISList()
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsActivity {
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
+ onCreate()
|
||||||
|
+ saveSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Controllers Factory" {
|
||||||
|
interface ControllersFactory {
|
||||||
|
+ createAppCoordinator() : AppCoordinator
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultControllersFactory {
|
||||||
|
+ createAppCoordinator() : AppCoordinator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Core Controllers" {
|
||||||
|
class AppCoordinator {
|
||||||
|
- Context context
|
||||||
|
- NMEAController nmeaController
|
||||||
|
- NetworkController networkController
|
||||||
|
- DataController dataController
|
||||||
|
- NotificationController notificationController
|
||||||
|
- CompassController compassController
|
||||||
|
- MapController mapController
|
||||||
|
- Vessel ownVessel
|
||||||
|
- List<AISVessel> aisVessels
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
+ initializeControllers()
|
||||||
|
+ startServices()
|
||||||
|
+ stopServices()
|
||||||
|
+ onVesselUpdated()
|
||||||
|
+ onAISVesselUpdated()
|
||||||
|
+ onDOPUpdated()
|
||||||
|
+ onDataReceived()
|
||||||
|
+ onNotificationShown()
|
||||||
|
+ onCompassChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
class NMEAController {
|
||||||
|
- Context context
|
||||||
|
- NMEAParser nmeaParser
|
||||||
|
- AndroidNMEAListener androidNmeaListener
|
||||||
|
- GPSLocationListener gpsLocationListener
|
||||||
|
- ExecutorService executor
|
||||||
|
+ startAndroidNMEAListener()
|
||||||
|
+ stopAndroidNMEAListener()
|
||||||
|
+ startGPSLocationListener()
|
||||||
|
+ stopGPSLocationListener()
|
||||||
|
+ parseNMEAData()
|
||||||
|
}
|
||||||
|
|
||||||
|
class NetworkController {
|
||||||
|
- Context context
|
||||||
|
- UDPListener udpListener
|
||||||
|
- ExecutorService executor
|
||||||
|
- int udpPort
|
||||||
|
- boolean isUDPEnabled
|
||||||
|
+ setUDPEnabled()
|
||||||
|
+ startUDPListener()
|
||||||
|
+ stopUDPListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DataController {
|
||||||
|
- Context context
|
||||||
|
- Repository repository
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- ExecutorService executor
|
||||||
|
+ restoreDataAsync()
|
||||||
|
+ saveVesselData()
|
||||||
|
+ saveAISData()
|
||||||
|
+ performDatabaseCleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationController {
|
||||||
|
- Context context
|
||||||
|
- NotificationService notificationService
|
||||||
|
+ notifyNewAISTarget()
|
||||||
|
+ notifySafetyMessage()
|
||||||
|
+ notifyGPSStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompassController {
|
||||||
|
- Context context
|
||||||
|
- CompassSensor compassSensor
|
||||||
|
- Handler uiHandler
|
||||||
|
+ startCompass()
|
||||||
|
+ stopCompass()
|
||||||
|
+ isCompassAvailable() : boolean
|
||||||
|
+ isCompassActive() : boolean
|
||||||
|
+ getCompassStatus() : String
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapController {
|
||||||
|
- Context context
|
||||||
|
- MapInterface currentMapInterface
|
||||||
|
- MapView mapView
|
||||||
|
- MapLibreMapView mapLibreView
|
||||||
|
- List<MapInterfaceChangeListener> listeners
|
||||||
|
+ addMapInterfaceChangeListener()
|
||||||
|
+ removeMapInterfaceChangeListener()
|
||||||
|
+ switchToYandexMaps()
|
||||||
|
+ switchToMapLibre()
|
||||||
|
+ getCurrentMapInterface() : MapInterface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "UI Binders" {
|
||||||
|
class MenuBinder {
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- MenuActions actions
|
||||||
|
+ onCreateOptionsMenu()
|
||||||
|
+ onPrepareOptionsMenu()
|
||||||
|
+ onOptionsItemSelected()
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomSheetsBinder {
|
||||||
|
- Context context
|
||||||
|
- BottomSheetDialog ownVesselBottomSheet
|
||||||
|
- BottomSheetDialog aisVesselBottomSheet
|
||||||
|
- View ownBottomSheetView
|
||||||
|
- View aisBottomSheetView
|
||||||
|
- AISVessel currentAISVessel
|
||||||
|
+ init()
|
||||||
|
+ initAIS()
|
||||||
|
+ showOwnVesselSheet()
|
||||||
|
+ showAISVesselSheet()
|
||||||
|
+ startAutoUpdate()
|
||||||
|
+ stopAutoUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
class BottomSheetsManager {
|
||||||
|
- Context context
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- BottomSheetDialog ownVesselBottomSheet
|
||||||
|
- BottomSheetDialog aisVesselBottomSheet
|
||||||
|
- View bottomSheetView
|
||||||
|
- View aisBottomSheetView
|
||||||
|
- AISVessel currentAISVessel
|
||||||
|
+ init()
|
||||||
|
+ showOwnVesselSheet()
|
||||||
|
+ showAISVesselSheet()
|
||||||
|
+ updateOwnVesselUI()
|
||||||
|
+ updateAISBottomSheetUI()
|
||||||
|
+ stopAutoUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
class PermissionsBinder {
|
||||||
|
- Activity activity
|
||||||
|
+ ensurePermission() : boolean
|
||||||
|
+ handleOnRequestPermissionsResult() : boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Data Processing" {
|
||||||
|
class NMEAParser {
|
||||||
|
- Vessel ownVessel
|
||||||
|
- List<AISVessel> aisVessels
|
||||||
|
- NMEAParserListener listener
|
||||||
|
- GPSLocationListener gpsLocationListener
|
||||||
|
- Map<String, Map<Integer, String>> aisFragments
|
||||||
|
- boolean hybridMode
|
||||||
|
+ parseNMEA()
|
||||||
|
+ setHybridMode()
|
||||||
|
+ setGPSLocationListener()
|
||||||
|
}
|
||||||
|
|
||||||
|
class UDPListener {
|
||||||
|
- int port
|
||||||
|
- DatagramSocket socket
|
||||||
|
- ExecutorService executor
|
||||||
|
- AtomicBoolean isRunning
|
||||||
|
- UDPListenerCallback callback
|
||||||
|
+ start()
|
||||||
|
+ stop()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AndroidNMEAListener {
|
||||||
|
- LocationManager locationManager
|
||||||
|
- NMEAMessageCallback callback
|
||||||
|
- boolean isListening
|
||||||
|
+ startListening() : boolean
|
||||||
|
+ stopListening()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class GPSLocationListener {
|
||||||
|
- Context context
|
||||||
|
- LocationManager locationManager
|
||||||
|
- LocationCallback callback
|
||||||
|
- boolean isListening
|
||||||
|
- int satelliteCount
|
||||||
|
- int activeSatellites
|
||||||
|
- double pdop
|
||||||
|
- double hdop
|
||||||
|
- double vdop
|
||||||
|
+ startListening() : boolean
|
||||||
|
+ stopListening()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class VesselPathController {
|
||||||
|
- Context context
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- SharedPreferences prefs
|
||||||
|
- String vesselId
|
||||||
|
- Handler uiHandler
|
||||||
|
- List<VesselPathPoint> pathPoints
|
||||||
|
- VesselPathPoint lastPoint
|
||||||
|
+ addPathPoint()
|
||||||
|
+ getPathPoints() : List<VesselPathPoint>
|
||||||
|
+ clearPath()
|
||||||
|
+ savePath()
|
||||||
|
+ loadPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Maps" {
|
||||||
|
interface MapInterface {
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
|
||||||
|
class YandexMapImpl {
|
||||||
|
- Context context
|
||||||
|
- MapView mapView
|
||||||
|
- MapObjectCollection mapObjects
|
||||||
|
- MarkerClickListener markerClickListener
|
||||||
|
- YandexMarkerManager markerManager
|
||||||
|
- CursorOverlay cursorOverlay
|
||||||
|
- Vessel ownVessel
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapLibreMapImpl {
|
||||||
|
- Context context
|
||||||
|
- MapView mapView
|
||||||
|
- MapLibreMap mapLibreMap
|
||||||
|
- MarkerClickListener markerClickListener
|
||||||
|
- CursorOverlay cursorOverlay
|
||||||
|
- Vessel ownVessel
|
||||||
|
- Map<String, AISVessel> aisVessels
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
|
||||||
|
class MapForgeImpl {
|
||||||
|
- Context context
|
||||||
|
- MapView mapView
|
||||||
|
- MarkerClickListener markerClickListener
|
||||||
|
- CursorOverlay cursorOverlay
|
||||||
|
- Vessel ownVessel
|
||||||
|
+ initialize()
|
||||||
|
+ cleanup()
|
||||||
|
+ addOwnVesselMarker()
|
||||||
|
+ updateOwnVesselPosition()
|
||||||
|
+ addAISVesselMarker()
|
||||||
|
+ updateAISVesselPosition()
|
||||||
|
+ removeAISVesselMarker()
|
||||||
|
+ clearAISVesselMarkers()
|
||||||
|
+ centerOnPosition()
|
||||||
|
+ setZoom()
|
||||||
|
+ getZoom() : float
|
||||||
|
+ setBearing()
|
||||||
|
+ getBearing() : float
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Data Models" {
|
||||||
|
class Vessel {
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double magneticCompass
|
||||||
|
- int signalStrength
|
||||||
|
- LocalDateTime lastUpdate
|
||||||
|
- String vesselName
|
||||||
|
- String mmsi
|
||||||
|
- String callSign
|
||||||
|
- double altitude
|
||||||
|
- int satellites
|
||||||
|
- int activeSatellites
|
||||||
|
- double pdop
|
||||||
|
- double hdop
|
||||||
|
- double vdop
|
||||||
|
- float accuracy
|
||||||
|
- long fixTime
|
||||||
|
- String fixQuality
|
||||||
|
+ updatePosition()
|
||||||
|
+ updateGPSQuality()
|
||||||
|
+ getGPSQualityPercentage() : int
|
||||||
|
+ getGPSQualityDescription() : String
|
||||||
|
}
|
||||||
|
|
||||||
|
class AISVessel {
|
||||||
|
- String mmsi
|
||||||
|
- String vesselName
|
||||||
|
- String callSign
|
||||||
|
- int imo
|
||||||
|
- String vesselType
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double rateOfTurn
|
||||||
|
- double length
|
||||||
|
- double width
|
||||||
|
- double draft
|
||||||
|
- String destination
|
||||||
|
- LocalDateTime eta
|
||||||
|
- LocalDateTime lastUpdate
|
||||||
|
- int signalStrength
|
||||||
|
- boolean isActive
|
||||||
|
- String navigationalStatus
|
||||||
|
- String lastSafetyMessage
|
||||||
|
- boolean positionAccuracy
|
||||||
|
- String vesselClass
|
||||||
|
- String vendorId
|
||||||
|
- boolean selected
|
||||||
|
+ updatePosition()
|
||||||
|
+ isDataStale() : boolean
|
||||||
|
+ shouldBeRemoved() : boolean
|
||||||
|
+ getMinutesSinceLastUpdate() : long
|
||||||
|
}
|
||||||
|
|
||||||
|
class VesselPathPoint {
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- long timestamp
|
||||||
|
+ VesselPathPoint()
|
||||||
|
+ toJSON() : String
|
||||||
|
+ fromJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Database" {
|
||||||
|
abstract class AppDatabase {
|
||||||
|
+ aisVesselDao() : AISVesselDao
|
||||||
|
+ vesselDao() : VesselDao
|
||||||
|
+ getInstance() : AppDatabase
|
||||||
|
}
|
||||||
|
|
||||||
|
class Repository {
|
||||||
|
- AISVesselDao aisVesselDao
|
||||||
|
- VesselDao vesselDao
|
||||||
|
- ExecutorService ioExecutor
|
||||||
|
+ upsertAIS()
|
||||||
|
+ deleteStaleAIS()
|
||||||
|
+ getAllAISSync() : List<AISVesselEntity>
|
||||||
|
+ observeAllAIS() : LiveData<List<AISVesselEntity>>
|
||||||
|
+ getAISByMmsiSync() : AISVesselEntity
|
||||||
|
+ upsertOwnVessel()
|
||||||
|
+ getLatestOwnVesselSync() : VesselEntity
|
||||||
|
+ getLatestOwnVesselAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AISVesselDao {
|
||||||
|
+ upsert()
|
||||||
|
+ deleteStale()
|
||||||
|
+ getAll() : List<AISVesselEntity>
|
||||||
|
+ observeAll() : LiveData<List<AISVesselEntity>>
|
||||||
|
+ getByMmsi() : AISVesselEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VesselDao {
|
||||||
|
+ upsert()
|
||||||
|
+ getLatest() : VesselEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
class AISVesselEntity {
|
||||||
|
- String mmsi
|
||||||
|
- String vesselName
|
||||||
|
- String callSign
|
||||||
|
- int imo
|
||||||
|
- String vesselType
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double rateOfTurn
|
||||||
|
- double length
|
||||||
|
- double width
|
||||||
|
- double draft
|
||||||
|
- String destination
|
||||||
|
- long etaEpochMs
|
||||||
|
- long lastUpdateEpochMs
|
||||||
|
- int signalStrength
|
||||||
|
- boolean isActive
|
||||||
|
- String navigationalStatus
|
||||||
|
- String lastSafetyMessage
|
||||||
|
- boolean positionAccuracy
|
||||||
|
- String vesselClass
|
||||||
|
- String vendorId
|
||||||
|
}
|
||||||
|
|
||||||
|
class VesselEntity {
|
||||||
|
- double latitude
|
||||||
|
- double longitude
|
||||||
|
- double course
|
||||||
|
- double speed
|
||||||
|
- double heading
|
||||||
|
- double magneticCompass
|
||||||
|
- int signalStrength
|
||||||
|
- long lastUpdateEpochMs
|
||||||
|
- String vesselName
|
||||||
|
- String mmsi
|
||||||
|
- String callSign
|
||||||
|
- double altitude
|
||||||
|
- int satellites
|
||||||
|
- int activeSatellites
|
||||||
|
- double pdop
|
||||||
|
- double hdop
|
||||||
|
- double vdop
|
||||||
|
- float accuracy
|
||||||
|
- long fixTime
|
||||||
|
- String fixQuality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "UI Components" {
|
||||||
|
class UIRenderingCoordinator {
|
||||||
|
- MapInterface mapInterface
|
||||||
|
- Handler uiHandler
|
||||||
|
- Vessel pendingVesselUpdate
|
||||||
|
- Map<String, AISVessel> pendingAISUpdates
|
||||||
|
- Set<String> pendingAISRemovals
|
||||||
|
- Runnable vesselUpdateRunnable
|
||||||
|
- Runnable aisUpdateRunnable
|
||||||
|
- Runnable pathUpdateRunnable
|
||||||
|
- boolean vesselUpdatePending
|
||||||
|
- boolean aisUpdatePending
|
||||||
|
- boolean pathUpdatePending
|
||||||
|
+ requestVesselUpdate()
|
||||||
|
+ requestAISUpdate()
|
||||||
|
+ requestAISRemoval()
|
||||||
|
+ flushPendingOperations()
|
||||||
|
+ cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UIDataChangeNotifier {
|
||||||
|
+ onVesselPositionChanged()
|
||||||
|
+ onGPSQualityChanged()
|
||||||
|
+ onAISVesselChanged()
|
||||||
|
+ onAISVesselRemoved()
|
||||||
|
+ onVesselPathChanged()
|
||||||
|
+ onRequestCenterMap()
|
||||||
|
+ onCompassUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompassView {
|
||||||
|
- float azimuth
|
||||||
|
- Paint compassPaint
|
||||||
|
- Paint needlePaint
|
||||||
|
- Paint textPaint
|
||||||
|
- List<AISVessel> nearbyVessels
|
||||||
|
+ setAzimuth()
|
||||||
|
+ setNearbyVessels()
|
||||||
|
+ onDraw()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CompassSensor {
|
||||||
|
- SensorManager sensorManager
|
||||||
|
- Sensor magnetometer
|
||||||
|
- Sensor accelerometer
|
||||||
|
- CompassListener callback
|
||||||
|
- float[] lastAccelerometer
|
||||||
|
- float[] lastMagnetometer
|
||||||
|
- boolean lastAccelerometerSet
|
||||||
|
- boolean lastMagnetometerSet
|
||||||
|
- float[] rotationMatrix
|
||||||
|
- float[] orientation
|
||||||
|
+ startListening()
|
||||||
|
+ stopListening()
|
||||||
|
+ setCallback()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CoordinatesDockWidget {
|
||||||
|
- TextView latitudeText
|
||||||
|
- TextView longitudeText
|
||||||
|
- TextView accuracyText
|
||||||
|
- TextView satellitesText
|
||||||
|
- TextView qualityText
|
||||||
|
+ updateCoordinates()
|
||||||
|
+ updateGPSQuality()
|
||||||
|
}
|
||||||
|
|
||||||
|
class CursorOverlay {
|
||||||
|
- ViewGroup parentView
|
||||||
|
- TextView coordinatesText
|
||||||
|
- TextView vesselInfoText
|
||||||
|
- boolean isVisible
|
||||||
|
+ show()
|
||||||
|
+ hide()
|
||||||
|
+ updateCoordinates()
|
||||||
|
+ setVesselInfo()
|
||||||
|
+ clearVesselInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Services" {
|
||||||
|
class NotificationService {
|
||||||
|
- Context context
|
||||||
|
- SettingsManager settingsManager
|
||||||
|
- Vibrator vibrator
|
||||||
|
- ToneGenerator toneGenerator
|
||||||
|
- boolean isInitialized
|
||||||
|
+ showSafetyAlert()
|
||||||
|
+ showNewVesselNotification()
|
||||||
|
+ clearNotifications()
|
||||||
|
+ setVibrationEnabled()
|
||||||
|
+ setSoundEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
class AISForegroundService {
|
||||||
|
- Context context
|
||||||
|
- AppCoordinator appCoordinator
|
||||||
|
- NotificationManager notificationManager
|
||||||
|
- boolean isRunning
|
||||||
|
+ startForeground()
|
||||||
|
+ stopForeground()
|
||||||
|
+ onStartCommand()
|
||||||
|
+ onDestroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
package "Utils" {
|
||||||
|
class SettingsManager {
|
||||||
|
- Context context
|
||||||
|
- SharedPreferences prefs
|
||||||
|
+ getUDPPort() : int
|
||||||
|
+ setUDPPort()
|
||||||
|
+ isUDPEnabled() : boolean
|
||||||
|
+ setUDPEnabled()
|
||||||
|
+ isAndroidNMEAEnabled() : boolean
|
||||||
|
+ setAndroidNMEAEnabled()
|
||||||
|
+ isUDPNMEAEnabled() : boolean
|
||||||
|
+ setUDPNMEAEnabled()
|
||||||
|
+ getDataMode() : String
|
||||||
|
+ setDataMode()
|
||||||
|
+ getDataStaleWarningMinutes() : int
|
||||||
|
+ setDataStaleWarningMinutes()
|
||||||
|
+ getDataStaleRemoveMinutes() : int
|
||||||
|
+ setDataStaleRemoveMinutes()
|
||||||
|
+ isPathTrackingEnabled() : boolean
|
||||||
|
+ setPathTrackingEnabled()
|
||||||
|
+ getPathColor() : int
|
||||||
|
+ setPathColor()
|
||||||
|
+ getPredictionColor() : int
|
||||||
|
+ setPredictionColor()
|
||||||
|
+ getPathWidth() : float
|
||||||
|
+ setPathWidth()
|
||||||
|
+ getPredictionWidth() : float
|
||||||
|
+ setPredictionWidth()
|
||||||
|
+ getPathMaxPoints() : int
|
||||||
|
+ setPathMaxPoints()
|
||||||
|
+ getPredictionHorizonSec() : int
|
||||||
|
+ setPredictionHorizonSec()
|
||||||
|
+ isVibrationEnabled() : boolean
|
||||||
|
+ setVibrationEnabled()
|
||||||
|
+ isSoundEnabled() : boolean
|
||||||
|
+ setSoundEnabled()
|
||||||
|
+ isKeepScreenOnEnabled() : boolean
|
||||||
|
+ setKeepScreenOnEnabled()
|
||||||
|
+ isCursorEnabled() : boolean
|
||||||
|
+ setCursorEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoUtils {
|
||||||
|
+ calculateDistance() : double
|
||||||
|
+ calculateBearing() : double
|
||||||
|
+ isValidCoordinate() : boolean
|
||||||
|
+ formatCoordinate() : String
|
||||||
|
+ convertToDecimalDegrees() : double
|
||||||
|
}
|
||||||
|
|
||||||
|
class LogSender {
|
||||||
|
+ sendLog()
|
||||||
|
+ sendError()
|
||||||
|
+ sendWarning()
|
||||||
|
+ sendInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MIDToCountry {
|
||||||
|
+ getCountryByMID() : String
|
||||||
|
+ getCountryName() : String
|
||||||
|
+ isValidMID() : boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class NavigationUtils {
|
||||||
|
+ calculateCourse() : double
|
||||||
|
+ calculateSpeed() : double
|
||||||
|
+ calculateETA() : LocalDateTime
|
||||||
|
+ isCollisionRisk() : boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
' Основные связи
|
||||||
|
MainActivity --> AppCoordinator : uses
|
||||||
|
MainActivity --> MenuBinder : uses
|
||||||
|
MainActivity --> BottomSheetsBinder : uses
|
||||||
|
MainActivity --> PermissionsBinder : uses
|
||||||
|
MainActivity --> MapController : uses
|
||||||
|
MainActivity --> CompassController : uses
|
||||||
|
MainActivity --> UIRenderingCoordinator : uses
|
||||||
|
MainActivity --> CompassView : uses
|
||||||
|
MainActivity --> CoordinatesDockWidget : uses
|
||||||
|
MainActivity --> BottomSheetsManager : uses
|
||||||
|
|
||||||
|
ControllersFactory <|.. DefaultControllersFactory : implements
|
||||||
|
MainActivity --> ControllersFactory : uses
|
||||||
|
DefaultControllersFactory --> AppCoordinator : creates
|
||||||
|
|
||||||
|
AppCoordinator --> NMEAController : coordinates
|
||||||
|
AppCoordinator --> NetworkController : coordinates
|
||||||
|
AppCoordinator --> DataController : coordinates
|
||||||
|
AppCoordinator --> NotificationController : coordinates
|
||||||
|
AppCoordinator --> CompassController : coordinates
|
||||||
|
AppCoordinator --> MapController : coordinates
|
||||||
|
AppCoordinator --> VesselPathController : uses
|
||||||
|
AppCoordinator --> SettingsManager : uses
|
||||||
|
AppCoordinator --> UIRenderingCoordinator : uses
|
||||||
|
|
||||||
|
NMEAController --> NMEAParser : uses
|
||||||
|
NMEAController --> AndroidNMEAListener : uses
|
||||||
|
NMEAController --> GPSLocationListener : uses
|
||||||
|
|
||||||
|
NetworkController --> UDPListener : uses
|
||||||
|
|
||||||
|
DataController --> Repository : uses
|
||||||
|
DataController --> SettingsManager : uses
|
||||||
|
|
||||||
|
NotificationController --> NotificationService : uses
|
||||||
|
|
||||||
|
CompassController --> CompassSensor : uses
|
||||||
|
|
||||||
|
MenuBinder --> AppCoordinator : uses
|
||||||
|
MenuBinder --> SettingsManager : uses
|
||||||
|
BottomSheetsBinder --> Context : uses
|
||||||
|
BottomSheetsManager --> AppCoordinator : uses
|
||||||
|
PermissionsBinder --> Activity : uses
|
||||||
|
|
||||||
|
MapController --> MapInterface : manages
|
||||||
|
MapController --> YandexMapImpl : creates
|
||||||
|
MapController --> MapLibreMapImpl : creates
|
||||||
|
MapController --> MapForgeImpl : creates
|
||||||
|
|
||||||
|
YandexMapImpl ..|> MapInterface : implements
|
||||||
|
MapLibreMapImpl ..|> MapInterface : implements
|
||||||
|
MapForgeImpl ..|> MapInterface : implements
|
||||||
|
|
||||||
|
Repository --> AppDatabase : uses
|
||||||
|
Repository --> AISVesselDao : uses
|
||||||
|
Repository --> VesselDao : uses
|
||||||
|
|
||||||
|
AppDatabase --> AISVesselEntity : contains
|
||||||
|
AppDatabase --> VesselEntity : contains
|
||||||
|
|
||||||
|
UIRenderingCoordinator ..|> UIDataChangeNotifier : implements
|
||||||
|
|
||||||
|
NMEAParser --> Vessel : creates/updates
|
||||||
|
NMEAParser --> AISVessel : creates/updates
|
||||||
|
NMEAParser --> GPSLocationListener : uses
|
||||||
|
|
||||||
|
VesselPathController --> VesselPathPoint : manages
|
||||||
|
VesselPathController --> SettingsManager : uses
|
||||||
|
|
||||||
|
NotificationService --> SettingsManager : uses
|
||||||
|
|
||||||
|
CompassSensor --> CompassView : updates
|
||||||
|
CompassView --> AISVessel : displays
|
||||||
|
|
||||||
|
@enduml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Описание упрощенной PlantUML диаграммы
|
||||||
|
|
||||||
|
### 🎯 **Исправления для совместимости:**
|
||||||
|
|
||||||
|
1. **Убрана тема** - удалена `!theme plain` которая могла вызывать проблемы
|
||||||
|
2. **Упрощены настройки** - оставлены только базовые skinparam
|
||||||
|
3. **Убраны сложные элементы** - удалены некоторые методы для упрощения
|
||||||
|
4. **Оптимизированы связи** - оставлены только основные связи
|
||||||
|
|
||||||
|
### 📦 **Структура пакетов:**
|
||||||
|
|
||||||
|
- **Main Activity** - основные активности приложения
|
||||||
|
- **Controllers Factory** - фабрика для создания контроллеров
|
||||||
|
- **Core Controllers** - основные контроллеры системы
|
||||||
|
- **UI Binders** - компоненты для управления UI
|
||||||
|
- **Data Processing** - обработка данных и парсинг
|
||||||
|
- **Maps** - система карт и маркеров
|
||||||
|
- **Data Models** - модели данных
|
||||||
|
- **Database** - слой базы данных
|
||||||
|
- **UI Components** - UI компоненты
|
||||||
|
- **Services** - сервисы приложения
|
||||||
|
- **Utils** - утилиты и вспомогательные классы
|
||||||
|
|
||||||
|
### 🔧 **Как использовать:**
|
||||||
|
|
||||||
|
1. **Онлайн редакторы:** PlantUML Online Server, PlantText
|
||||||
|
2. **IDE плагины:** IntelliJ IDEA, VS Code, Eclipse
|
||||||
|
3. **Командная строка:** PlantUML jar файл
|
||||||
|
4. **Документация:** GitLab, GitHub поддерживают PlantUML
|
||||||
|
|
||||||
|
### ✅ **Преимущества упрощенной версии:**
|
||||||
|
|
||||||
|
- Более совместима с различными PlantUML редакторами
|
||||||
|
- Меньше вероятность ошибок с Graphviz
|
||||||
|
- Сохраняет всю основную архитектуру
|
||||||
|
- Легче для понимания и презентации
|
||||||
|
|
||||||
|
Эта упрощенная версия должна работать без проблем с Graphviz и показывать всю архитектуру вашего приложения.
|
||||||
Reference in New Issue
Block a user