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.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
@@ -27,6 +29,9 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
||||
private Runnable tickerRunnable;
|
||||
private android.widget.TextView textEmptyState;
|
||||
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;
|
||||
@@ -46,10 +51,25 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
||||
recyclerView = findViewById(R.id.recycler_ais_targets);
|
||||
textEmptyState = findViewById(R.id.text_empty_state);
|
||||
textTargetCount = findViewById(R.id.text_target_count);
|
||||
editSearch = findViewById(R.id.edit_search);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
adapter = new AisTargetsAdapter(new ArrayList<>(), this);
|
||||
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>>() {
|
||||
@Override
|
||||
public void onChanged(List<AISVesselEntity> entities) {
|
||||
@@ -57,23 +77,12 @@ public class AisTargetsActivity extends AppCompatActivity implements AisTargetsA
|
||||
if (entities != null) {
|
||||
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);
|
||||
|
||||
// Обновляем счетчик целей
|
||||
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);
|
||||
}
|
||||
|
||||
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() {
|
||||
repository.getLatestOwnVesselAsync(new Repository.RepositoryCallback<VesselEntity>() {
|
||||
@Override
|
||||
|
||||
@@ -77,6 +77,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private ImageButton btnCenterOnVessel;
|
||||
private ImageButton btnMapOrientation;
|
||||
private ImageButton btnCursorToggle;
|
||||
private ImageButton btnSettings;
|
||||
private ImageButton btnAisTargets;
|
||||
private LinearLayout controlPanel;
|
||||
@@ -88,6 +89,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
private Runnable compassUpdateRunnable;
|
||||
private Runnable coordinatesUpdateRunnable;
|
||||
private Runnable compassButtonRotationRunnable;
|
||||
private static final long COMPASS_ANIM_DURATION_MS = 150;
|
||||
private Vessel lastCompassVessel;
|
||||
private Vessel lastCoordinatesVessel;
|
||||
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);
|
||||
btnCenterOnVessel = findViewById(R.id.btn_center_vessel);
|
||||
btnMapOrientation = findViewById(R.id.btn_map_orientation);
|
||||
btnCursorToggle = findViewById(R.id.btn_cursor_toggle);
|
||||
btnSettings = findViewById(R.id.btn_settings);
|
||||
btnAisTargets = findViewById(R.id.btn_ais_targets);
|
||||
controlPanel = findViewById(R.id.control_panel);
|
||||
@@ -178,18 +181,36 @@ public class MainActivity extends AppCompatActivity {
|
||||
coordinatesWidget.updateVessel(lastCoordinatesVessel);
|
||||
}
|
||||
};
|
||||
// Периодическое обновление поворота кнопки компаса по bearing карты
|
||||
// Периодическое обновление поворота кнопок (компас и ownship)
|
||||
compassButtonRotationRunnable = () -> {
|
||||
try {
|
||||
if (btnMapOrientation != null && mapController.getCurrentMapInterface() != null) {
|
||||
float bearing = mapController.getCurrentMapInterface().getBearing();
|
||||
// Иконка должна указывать север: вращаем противоположно bearing карты
|
||||
btnMapOrientation.setRotation(-bearing);
|
||||
MapInterface mapIf = mapController != null ? mapController.getCurrentMapInterface() : null;
|
||||
if (mapIf != null) {
|
||||
float mapBearing = 0f;
|
||||
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) {}
|
||||
// Планируем следующее обновление
|
||||
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);
|
||||
@@ -211,6 +232,8 @@ public class MainActivity extends AppCompatActivity {
|
||||
private void setupButtonListeners() {
|
||||
if (btnCenterOnVessel != null) btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
|
||||
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 (btnAisTargets != null) btnAisTargets.setOnClickListener(v -> openAisTargets());
|
||||
|
||||
@@ -297,12 +320,31 @@ public class MainActivity extends AppCompatActivity {
|
||||
compassView.post(() -> {
|
||||
updateControlPanelPosition();
|
||||
});
|
||||
// Стартуем обновление поворота кнопки компаса
|
||||
if (uiThrottleHandler != null) {
|
||||
// Стартуем цикл обновления поворотов кнопок
|
||||
startCompassButtonsLoop();
|
||||
}
|
||||
|
||||
private void startCompassButtonsLoop() {
|
||||
if (uiThrottleHandler != null && compassButtonRotationRunnable != null) {
|
||||
uiThrottleHandler.removeCallbacks(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() {
|
||||
// Настраиваем слушатель изменения размера dock-виджета
|
||||
@@ -322,30 +364,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
updateControlPanelPosition();
|
||||
});
|
||||
|
||||
// Устанавливаем виджет координат в dock-режим внизу экрана
|
||||
// Устанавливаем виджет координат в dock-режим внизу экрана без тестовых данных
|
||||
coordinatesWidget.post(() -> {
|
||||
Log.d(TAG, "Setting coordinates widget to dock mode");
|
||||
coordinatesWidget.setDocked(true, false, 0, 0); // false = dock снизу
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -904,6 +927,31 @@ public class MainActivity extends AppCompatActivity {
|
||||
Intent intent = new Intent(this, AisTargetsActivity.class);
|
||||
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
|
||||
@@ -1081,23 +1129,26 @@ public class MainActivity extends AppCompatActivity {
|
||||
// Обрабатываем возможный интент центрирования
|
||||
handleCenterIntentIfAny(getIntent());
|
||||
|
||||
// Восстанавливаем курсор после возврата в активность
|
||||
// Восстанавливаем курсор после возврата в активность согласно настройке
|
||||
if (mapController.getCurrentMapInterface() != null) {
|
||||
boolean cursorEnabled = settingsManager.isCursorEnabled();
|
||||
if (cursorEnabled) {
|
||||
mapController.getCurrentMapInterface().showCursor();
|
||||
// Обновляем координаты курсора с центра карты
|
||||
mapController.getCurrentMapInterface().updateCursorFromMapCenter();
|
||||
|
||||
// Принудительно проверяем AIS судно под курсором для восстановления панели
|
||||
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||
((MapLibreMapImpl) mapController.getCurrentMapInterface()).forceCheckAisVesselUnderCursor();
|
||||
}
|
||||
} else {
|
||||
// Явно скрываем при возвращении, если выключен в настройках
|
||||
mapController.getCurrentMapInterface().hideCursor();
|
||||
}
|
||||
}
|
||||
|
||||
// Проверяем разрешения и запускаем контроллеры
|
||||
checkPermissions();
|
||||
|
||||
// Перезапускаем цикл поворота кнопок после возврата в активити
|
||||
startCompassButtonsLoop();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1148,6 +1199,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
// Очищаем информацию о AIS судне при паузе активности
|
||||
if (mapController.getCurrentMapInterface() != null) {
|
||||
mapController.getCurrentMapInterface().clearAisVesselInfo();
|
||||
// Принудительно удаляем overlay курсора, чтобы не накапливался между активити
|
||||
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||
((MapLibreMapImpl) mapController.getCurrentMapInterface()).removeCursorOverlay();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1324,6 +1379,11 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
// Применяем настройки курсора
|
||||
applyCursorSettings(cursorEnabled);
|
||||
// Применяем дебаг-режим на карте (красный квадрат)
|
||||
boolean debugEnabled = data.getBooleanExtra("debug_enabled", settingsManager.isDebugEnabled());
|
||||
if (mapController.getCurrentMapInterface() instanceof MapLibreMapImpl) {
|
||||
((MapLibreMapImpl) mapController.getCurrentMapInterface()).setDebugMode(debugEnabled);
|
||||
}
|
||||
|
||||
if (needsRestart) {
|
||||
Log.i(TAG, "Требуется перезапуск сервисов");
|
||||
|
||||
@@ -38,7 +38,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
private SwitchMaterial switchVibrationEnabled;
|
||||
private SwitchMaterial switchSoundEnabled;
|
||||
private SwitchMaterial switchKeepScreenOn;
|
||||
private SwitchMaterial switchCursorEnabled;
|
||||
private SwitchMaterial switchDebugEnabled;
|
||||
private Button btnCancel;
|
||||
private Button btnSave;
|
||||
private Button btnClearPath;
|
||||
@@ -104,7 +104,7 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
switchVibrationEnabled = findViewById(R.id.switch_vibration_enabled);
|
||||
switchSoundEnabled = findViewById(R.id.switch_sound_enabled);
|
||||
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);
|
||||
btnSave = findViewById(R.id.btn_save);
|
||||
btnClearPath = findViewById(R.id.btn_clear_path);
|
||||
@@ -154,8 +154,8 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
// Настройки экрана
|
||||
switchKeepScreenOn.setChecked(settingsManager.isKeepScreenOnEnabled());
|
||||
|
||||
// Настройки курсора
|
||||
switchCursorEnabled.setChecked(settingsManager.isCursorEnabled());
|
||||
// Дебаг
|
||||
switchDebugEnabled.setChecked(settingsManager.isDebugEnabled());
|
||||
|
||||
// Путь и предсказание
|
||||
etPathMaxPoints.setText(String.valueOf(settingsManager.getPathMaxPoints()));
|
||||
@@ -182,7 +182,6 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
originalVibrationEnabled = settingsManager.isVibrationEnabled();
|
||||
originalSoundEnabled = settingsManager.isSoundEnabled();
|
||||
originalKeepScreenOnEnabled = settingsManager.isKeepScreenOnEnabled();
|
||||
originalCursorEnabled = settingsManager.isCursorEnabled();
|
||||
|
||||
Log.i(TAG, "Оригинальные настройки сохранены");
|
||||
}
|
||||
@@ -314,7 +313,8 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
settingsManager.setVibrationEnabled(switchVibrationEnabled.isChecked());
|
||||
settingsManager.setSoundEnabled(switchSoundEnabled.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) {}
|
||||
@@ -338,7 +338,8 @@ public class SettingsActivity extends AppCompatActivity {
|
||||
resultIntent.putExtra("android_nmea_enabled", switchAndroidNMEAEnabled.isChecked());
|
||||
resultIntent.putExtra("udp_nmea_enabled", switchUDPNMEAEnabled.isChecked());
|
||||
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);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import com.grigowashere.aismap.utils.SettingsManager;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
@@ -42,7 +43,7 @@ public class AppCoordinator implements
|
||||
|
||||
// Состояние приложения
|
||||
private Vessel ownVessel;
|
||||
private List<AISVessel> aisVessels;
|
||||
private Map<String, AISVessel> aisVessels;
|
||||
private Map<String, VesselPathController> aisPathControllers;
|
||||
private SettingsManager settingsManager;
|
||||
private VesselPathController pathController;
|
||||
@@ -72,7 +73,7 @@ public class AppCoordinator implements
|
||||
public AppCoordinator(Context context) {
|
||||
this.context = context;
|
||||
this.ownVessel = new Vessel();
|
||||
this.aisVessels = new ArrayList<>();
|
||||
this.aisVessels = new LinkedHashMap<>();
|
||||
this.aisPathControllers = new HashMap<>();
|
||||
this.settingsManager = new SettingsManager(context);
|
||||
this.pathController = new VesselPathController(context, settingsManager);
|
||||
@@ -268,20 +269,16 @@ public class AppCoordinator implements
|
||||
}
|
||||
}
|
||||
|
||||
// Обновляем дополнительные данные
|
||||
if (vessel.getCourse() > 0) {
|
||||
ownVessel.setCourse(vessel.getCourse());
|
||||
updateCompass();
|
||||
}
|
||||
if (vessel.getSpeed() > 0) {
|
||||
ownVessel.setSpeed(vessel.getSpeed());
|
||||
}
|
||||
if (vessel.getSatellites() > 0) {
|
||||
ownVessel.setSatellites(vessel.getSatellites());
|
||||
}
|
||||
if (vessel.getAltitude() != 0) {
|
||||
ownVessel.setAltitude(vessel.getAltitude());
|
||||
}
|
||||
// Обновляем дополнительные данные (без фильтров по нулю, чтобы корректно обрабатывать сбросы)
|
||||
ownVessel.setCourse(vessel.getCourse());
|
||||
ownVessel.setSpeed(vessel.getSpeed());
|
||||
ownVessel.setSatellites(vessel.getSatellites());
|
||||
ownVessel.setActiveSatellites(vessel.getActiveSatellites());
|
||||
ownVessel.setAltitude(vessel.getAltitude());
|
||||
ownVessel.setFixQuality(vessel.getFixQuality());
|
||||
|
||||
// Обновляем компас после изменения курса
|
||||
updateCompass();
|
||||
|
||||
// Сохраняем в БД
|
||||
dataController.saveVesselPosition(ownVessel);
|
||||
@@ -343,8 +340,10 @@ public class AppCoordinator implements
|
||||
}
|
||||
} else {
|
||||
// Добавляем новое судно
|
||||
synchronized (aisVessels) {
|
||||
aisVessels.add(vessel);
|
||||
if (vessel.getMmsi() != null) {
|
||||
synchronized (aisVessels) {
|
||||
aisVessels.put(vessel.getMmsi(), vessel);
|
||||
}
|
||||
}
|
||||
|
||||
// Если это новое судно сразу пришло с safety-сообщением — уведомим
|
||||
@@ -404,7 +403,11 @@ public class AppCoordinator implements
|
||||
if (aisVessels != null) {
|
||||
synchronized (this.aisVessels) {
|
||||
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()) {
|
||||
Log.i(TAG, "🚢 Восстанавливаем " + aisVessels.size() + " AIS судов");
|
||||
synchronized (aisVessels) {
|
||||
for (AISVessel vessel : aisVessels) {
|
||||
for (AISVessel vessel : aisVessels.values()) {
|
||||
uiDataNotifier.onAISVesselChanged(vessel);
|
||||
}
|
||||
}
|
||||
@@ -522,21 +525,19 @@ public class AppCoordinator implements
|
||||
// Вспомогательные методы
|
||||
|
||||
private AISVessel findAISVesselByMMSI(String mmsi) {
|
||||
synchronized (aisVessels) {
|
||||
for (AISVessel vessel : aisVessels) {
|
||||
if (mmsi.equals(vessel.getMmsi())) {
|
||||
return vessel;
|
||||
}
|
||||
}
|
||||
if (mmsi == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (aisVessels) {
|
||||
return aisVessels.get(mmsi);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private List<AISVessel> getNearbyVessels() {
|
||||
List<AISVessel> nearby = new ArrayList<>();
|
||||
double maxDistance = 10000; // 10 км в метрах
|
||||
|
||||
for (AISVessel vessel : aisVessels) {
|
||||
for (AISVessel vessel : aisVessels.values()) {
|
||||
double distance = com.grigowashere.aismap.utils.GeoUtils.calculateDistance(ownVessel, vessel);
|
||||
if (distance <= maxDistance) {
|
||||
nearby.add(vessel);
|
||||
@@ -631,7 +632,7 @@ public class AppCoordinator implements
|
||||
|
||||
public List<AISVessel> getAISVessels() {
|
||||
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.ArrayList;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
/**
|
||||
* Контроллер для парсинга NMEA сообщений
|
||||
@@ -174,10 +179,97 @@ public class NMEAParser {
|
||||
*/
|
||||
private String getField(String[] fields, int index) {
|
||||
if (index < fields.length && !fields[index].trim().isEmpty()) {
|
||||
return fields[index].trim();
|
||||
return sanitizeField(fields[index].trim());
|
||||
}
|
||||
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 значение из поля
|
||||
@@ -311,7 +403,7 @@ public class NMEAParser {
|
||||
}
|
||||
}
|
||||
|
||||
ownVessel.setSatellites(satellites);
|
||||
ownVessel.setActiveSatellites(satellites);
|
||||
ownVessel.setAltitude(altitude);
|
||||
|
||||
// Синхронизируем с GPSLocationListener для получения активных спутников
|
||||
@@ -343,6 +435,16 @@ public class NMEAParser {
|
||||
// Поле 8: курс в градусах
|
||||
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 данных
|
||||
|
||||
// В гибридном режиме не обновляем координаты
|
||||
@@ -414,31 +516,57 @@ public class NMEAParser {
|
||||
* Формат: $GPGLL,lat,N/S,lon,E/W,time,status,mode*checksum
|
||||
*/
|
||||
private void parseGLL(String[] fields) {
|
||||
if (hybridMode) {
|
||||
// Убираем шумный лог игнорирования GLL
|
||||
return;
|
||||
// Разбираем время фикса (поле 5), статус (поле 6) и режим (поле 7)
|
||||
String utcTimeStr = getField(fields, 5); // hhmmss.ss
|
||||
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
|
||||
|
||||
// Поля 1,2: широта и направление
|
||||
String latStr = getField(fields, 1);
|
||||
String latDir = getField(fields, 2);
|
||||
if (latStr != null && latDir != null) {
|
||||
double latitude = parseCoordinate(latStr, latDir.equals("N"));
|
||||
ownVessel.setLatitude(latitude);
|
||||
|
||||
// GLL не содержит дату — epoch не пишем, но строковое время сохраним
|
||||
if (utcTimeStr != null && utcTimeStr.length() >= 6) {
|
||||
ownVessel.setFixTimeNmea(utcTimeStr);
|
||||
}
|
||||
|
||||
// Поля 3,4: долгота и направление
|
||||
String lonStr = getField(fields, 3);
|
||||
String lonDir = getField(fields, 4);
|
||||
if (lonStr != null && lonDir != null) {
|
||||
double longitude = parseCoordinate(lonStr, lonDir.equals("E"));
|
||||
ownVessel.setLongitude(longitude);
|
||||
|
||||
// Если не в гибридном режиме — обновляем координаты
|
||||
if (!hybridMode) {
|
||||
// Поля 1,2: широта и направление
|
||||
String latStr = getField(fields, 1);
|
||||
String latDir = getField(fields, 2);
|
||||
if (latStr != null && latDir != null) {
|
||||
double latitude = parseCoordinate(latStr, "N".equals(latDir));
|
||||
ownVessel.setLatitude(latitude);
|
||||
}
|
||||
|
||||
// Поля 3,4: долгота и направление
|
||||
String lonStr = getField(fields, 3);
|
||||
String lonDir = getField(fields, 4);
|
||||
if (lonStr != null && lonDir != null) {
|
||||
double longitude = parseCoordinate(lonStr, "E".equals(lonDir));
|
||||
ownVessel.setLongitude(longitude);
|
||||
}
|
||||
}
|
||||
|
||||
// Убираем шумный лог GLL координат
|
||||
|
||||
|
||||
if (listener != null) {
|
||||
listener.onVesselUpdated(ownVessel);
|
||||
}
|
||||
@@ -470,7 +598,7 @@ public class NMEAParser {
|
||||
}
|
||||
|
||||
// Убираем шумный лог GSV спутников
|
||||
|
||||
int totalSatellites1 = 0;
|
||||
// Парсим данные о спутниках (начиная с поля 4, каждые 4 поля = 1 спутник)
|
||||
for (int i = 4; i < fields.length - 1; i += 4) { // -1 чтобы исключить контрольную сумму
|
||||
if (i + 3 < fields.length) {
|
||||
@@ -478,7 +606,7 @@ public class NMEAParser {
|
||||
String elevation = getField(fields, i + 1);
|
||||
String azimuth = getField(fields, i + 2);
|
||||
String snr = getField(fields, i + 3);
|
||||
|
||||
totalSatellites1++;
|
||||
if (satId != null) {
|
||||
Log.d(TAG, String.format("Спутник %s: elev=%s, azim=%s, SNR=%s",
|
||||
satId, elevation, azimuth, snr));
|
||||
@@ -507,7 +635,7 @@ public class NMEAParser {
|
||||
|
||||
// Обновляем общее количество спутников
|
||||
int totalSatellites = gpsSatellites + glonassSatellites + galileoSatellites;
|
||||
ownVessel.setSatellites(totalSatellites);
|
||||
ownVessel.setSatellites(totalSatellites1);
|
||||
|
||||
// Синхронизируем с GPSLocationListener для получения активных спутников
|
||||
if (gpsLocationListener != null) {
|
||||
@@ -557,7 +685,7 @@ public class NMEAParser {
|
||||
}
|
||||
}
|
||||
|
||||
ownVessel.setSatellites(satellites);
|
||||
ownVessel.setActiveSatellites(satellites);
|
||||
ownVessel.setAltitude(altitude);
|
||||
|
||||
// Синхронизируем с GPSLocationListener для получения активных спутников
|
||||
@@ -592,6 +720,14 @@ public class NMEAParser {
|
||||
|
||||
Log.d(TAG, String.format("ZDA: %04d-%02d-%02d %s, TZ: %+03d:%02d",
|
||||
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());
|
||||
@@ -613,6 +749,17 @@ public class NMEAParser {
|
||||
private void parseGSA(String[] fields) {
|
||||
// Убираем шумный лог парсинга 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 спутников)
|
||||
int activeSatellites = 0;
|
||||
for (int i = 3; i <= 14 && i < fields.length; i++) {
|
||||
@@ -623,6 +770,7 @@ public class NMEAParser {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Получаем DOP значения - могут быть в разных позициях в зависимости от количества полей
|
||||
double pdop = 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.R;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.View;
|
||||
import android.view.ViewParent;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
@@ -193,14 +195,39 @@ public class MapLibreMapImpl implements MapInterface {
|
||||
// VesselPathController будет установлен через setVesselPathController()
|
||||
this.pathController = null;
|
||||
this.cursorOverlay = new CursorOverlay(context);
|
||||
// Инициализируем флаг отладки из настроек
|
||||
this.debugMode = settingsManager.isDebugEnabled();
|
||||
|
||||
// Добавляем overlay курсора в MapView
|
||||
if (mapView instanceof ViewGroup) {
|
||||
ViewGroup parent = (ViewGroup) mapView;
|
||||
// Проверяем, не добавлен ли уже курсор
|
||||
if (parent.findViewById(R.id.cursor_cross) == null) {
|
||||
parent.addView(cursorOverlay.getView());
|
||||
// Добавляем overlay курсора НАД картой: в родителя MapView, чтобы он был поверх GL слоя
|
||||
ViewParent mapParent = mapView.getParent();
|
||||
if (mapParent instanceof ViewGroup) {
|
||||
ViewGroup parent = (ViewGroup) mapParent;
|
||||
// Удаляем все прежние cursor_overlay из родителя (на случай повторной инициализации)
|
||||
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 для карты
|
||||
@@ -288,7 +315,19 @@ public class MapLibreMapImpl implements MapInterface {
|
||||
if (maplibreMap != null) {
|
||||
maplibreMap.removeOnMapClickListener(onMapClickListener);
|
||||
}
|
||||
// Удаляем overlay курсора из родителя, чтобы не накапливался между активити
|
||||
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();
|
||||
}
|
||||
staleHandler.removeCallbacks(staleRunnable);
|
||||
@@ -1062,30 +1101,28 @@ public class MapLibreMapImpl implements MapInterface {
|
||||
Log.d(TAG, "appendOwnPathPoint: skipping zero coordinates");
|
||||
return;
|
||||
}
|
||||
|
||||
// ВРЕМЕННО ОТКЛЮЧАЕМ ПРОВЕРКУ РАССТОЯНИЯ ДЛЯ ТЕСТИРОВАНИЯ
|
||||
// Проверяем, изменились ли координаты (строгая проверка на дублирование)
|
||||
// if (ownPathCoords.length() > 0) {
|
||||
// JSONArray lastPoint = ownPathCoords.getJSONArray(ownPathCoords.length() - 1);
|
||||
// double lastLon = lastPoint.getDouble(0);
|
||||
// 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 (ownPathCoords.length() > 0) {
|
||||
JSONArray lastPoint = ownPathCoords.getJSONArray(ownPathCoords.length() - 1);
|
||||
double lastLon = lastPoint.getDouble(0);
|
||||
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 = settingsManager.getPathMaxPoints();
|
||||
if (ownPathCoords.length() >= max) {
|
||||
// удаляем из начала
|
||||
ownPathCoords.remove(0);
|
||||
@@ -1940,15 +1977,42 @@ public class MapLibreMapImpl implements MapInterface {
|
||||
|
||||
@Override
|
||||
public void showCursor() {
|
||||
if (cursorOverlay != null) {
|
||||
try {
|
||||
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
|
||||
public void hideCursor() {
|
||||
if (cursorOverlay != null) {
|
||||
cursorOverlay.hideCursor();
|
||||
clearAisVesselInfo();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1961,24 +2025,63 @@ public class MapLibreMapImpl implements MapInterface {
|
||||
|
||||
@Override
|
||||
public void updateCursorFromMapCenter() {
|
||||
if (cursorOverlay != null && maplibreMap != null && mapView != null) {
|
||||
if (cursorOverlay != null && mapView != null) {
|
||||
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",
|
||||
center.getLatitude(), center.getLongitude()));
|
||||
center != null ? center.getLatitude() : 0,
|
||||
center != null ? center.getLongitude() : 0));
|
||||
|
||||
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
|
||||
|
||||
// Проверяем, есть ли AIS судно под курсором
|
||||
checkAisVesselUnderCursor(center);
|
||||
if (center != null) {
|
||||
cursorOverlay.updateCursorCoordinates(center.getLatitude(), center.getLongitude());
|
||||
// Проверяем, есть ли AIS судно под курсором
|
||||
checkAisVesselUnderCursor(center);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "updateCursorFromMapCenter: MapView may be destroyed: " + e.getMessage());
|
||||
}
|
||||
} 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 судно под курсором (в центре экрана)
|
||||
|
||||
@@ -30,6 +30,7 @@ public class Vessel {
|
||||
private float accuracy; // точность в метрах
|
||||
private long fixTime; // время последнего фикса
|
||||
private String fixQuality; // качество фикса (GPS, DGPS, RTK и т.д.)
|
||||
private String fixTimeNmea; // время фикса как пришло в NMEA (строка HHMMSS[.SS])
|
||||
|
||||
public Vessel() {
|
||||
this.lastUpdate = LocalDateTime.now();
|
||||
@@ -104,6 +105,9 @@ public class Vessel {
|
||||
public String getFixQuality() { return 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 (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 (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()) : "🔒 Качество фикса: --");
|
||||
}
|
||||
|
||||
|
||||
@@ -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_CURSOR_ENABLED = "cursor_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";
|
||||
|
||||
// Значения по умолчанию
|
||||
@@ -56,6 +57,7 @@ public class SettingsManager {
|
||||
private static final boolean DEFAULT_CURSOR_ENABLED = false;
|
||||
private static final boolean DEFAULT_NOTIFICATIONS_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";
|
||||
@@ -468,5 +470,20 @@ public class SettingsManager {
|
||||
prefs.edit().putBoolean(KEY_NOTIFICATIONS_ENABLED, enabled).apply();
|
||||
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 scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
|
||||
|
||||
// Простой текст для проверки
|
||||
// Простой текст для проверки (убрана надпись "КОМПАС")
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setTextSize(24 * scaleFactor);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
canvas.drawText("КОМПАС", w/2, h/2, paint);
|
||||
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
|
||||
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
|
||||
float topTextY = dp(18) * scaleFactor;
|
||||
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, topTextY, paint);
|
||||
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, topTextY + 24 * scaleFactor, paint);
|
||||
|
||||
// Плавное обновление азимута
|
||||
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
|
||||
@@ -157,7 +157,10 @@ public class CompassView extends BaseDockWidget {
|
||||
if (degree % 45 == 0) {
|
||||
int directionIndex = (degree / 45) % 8;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ public class CursorOverlay {
|
||||
private void initializeViews() {
|
||||
LayoutInflater inflater = LayoutInflater.from(context);
|
||||
overlayView = inflater.inflate(R.layout.cursor, null);
|
||||
// Помечаем overlay для последующего обнаружения/удаления
|
||||
overlayView.setTag("cursor_overlay");
|
||||
|
||||
tvCursorLatitude = overlayView.findViewById(R.id.tv_cursor_latitude);
|
||||
tvCursorLongitude = overlayView.findViewById(R.id.tv_cursor_longitude);
|
||||
@@ -350,6 +352,10 @@ public class CursorOverlay {
|
||||
public void hideCursor() {
|
||||
if (overlayView != null) {
|
||||
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() {
|
||||
if (overlayView != null) {
|
||||
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: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
|
||||
android:id="@+id/text_target_count"
|
||||
android:layout_width="match_parent"
|
||||
|
||||
@@ -48,6 +48,17 @@
|
||||
android:scaleType="fitCenter"
|
||||
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
|
||||
android:id="@+id/btn_settings"
|
||||
android:layout_width="40dp"
|
||||
|
||||
@@ -545,7 +545,7 @@
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Настройки курсора -->
|
||||
<!-- Дебаг-режим -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -562,7 +562,7 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="🎯 Курсор на карте"
|
||||
android:text="🐞 Режим отладки"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
@@ -571,29 +571,20 @@
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Настройте отображение курсора с координатами центра экрана:"
|
||||
android:text="Включите расширенное логирование и диагностические элементы UI."
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/darker_gray"
|
||||
android:layout_marginBottom="16dp" />
|
||||
|
||||
<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_height="wrap_content"
|
||||
android:text="Показать курсор"
|
||||
android:text="Включить дебаг-режим"
|
||||
android:textSize="16sp"
|
||||
android:checked="false"
|
||||
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>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
android:id="@+id/coordinates_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/panel_background"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:visibility="visible">
|
||||
@@ -29,7 +29,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="Широта: --"
|
||||
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
|
||||
android:id="@+id/tv_cursor_longitude"
|
||||
@@ -38,7 +43,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="Долгота: --"
|
||||
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>
|
||||
|
||||
@@ -47,7 +57,7 @@
|
||||
android:id="@+id/distance_bearing_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/panel_background"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:visibility="visible">
|
||||
@@ -60,6 +70,11 @@
|
||||
android:text="Rnd:"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="2"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
@@ -71,6 +86,11 @@
|
||||
android:text="Brg: --"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:shadowColor="#000000"
|
||||
android:shadowDx="0"
|
||||
android:shadowDy="0"
|
||||
android:shadowRadius="2"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
|
||||
@@ -81,7 +101,7 @@
|
||||
android:id="@+id/ais_vessel_info_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/panel_background"
|
||||
android:background="@android:color/transparent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
android:visibility="gone">
|
||||
@@ -93,7 +113,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="MMSI: --"
|
||||
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
|
||||
android:id="@+id/tv_ais_name"
|
||||
@@ -102,7 +127,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="Название: --"
|
||||
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
|
||||
android:id="@+id/tv_ais_call_sign"
|
||||
@@ -111,7 +141,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="Позывной: --"
|
||||
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
|
||||
android:id="@+id/tv_ais_cog"
|
||||
@@ -120,7 +155,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="COG: --"
|
||||
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
|
||||
android:id="@+id/tv_ais_sog"
|
||||
@@ -129,7 +169,12 @@
|
||||
android:fontFamily="monospace"
|
||||
android:text="SOG: --"
|
||||
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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user