generated from Grigo/AndroidTemplate
Подготовка к крупным изменениям: карта, AIS и UI
- Яндекс/MapForge: правки в менеджерах и обёртках маркеров (улучшена отрисовка/логика) - NMEAParser: корректировки парсинга и стабильности - Модель AISVessel: уточнение полей/логики - Настройки: правки в SettingsActivity и SettingsManager, актуализация AppController - UI: обновлены activity_main, activity_settings, bottom_sheet_ais_vessel; меню main_menu - Ресурсы: добавлен drawable/targetclassa.xml, обновлён drawable/target.xml - Конфигурация: правки AndroidManifest и app/build.gradle - Прочее: изменения в .idea (не влияют на сборку)
This commit is contained in:
Generated
+8
@@ -4,6 +4,14 @@
|
|||||||
<selectionStates>
|
<selectionStates>
|
||||||
<SelectionState runConfigName="app">
|
<SelectionState runConfigName="app">
|
||||||
<option name="selectionMode" value="DROPDOWN" />
|
<option name="selectionMode" value="DROPDOWN" />
|
||||||
|
<DropdownSelection timestamp="2025-09-15T06:25:47.522835900Z">
|
||||||
|
<Target type="DEFAULT_BOOT">
|
||||||
|
<handle>
|
||||||
|
<DeviceId pluginId="Default" identifier="serial=192.168.22.44:5555;connection=ad165724" />
|
||||||
|
</handle>
|
||||||
|
</Target>
|
||||||
|
</DropdownSelection>
|
||||||
|
<DialogSelection />
|
||||||
</SelectionState>
|
</SelectionState>
|
||||||
</selectionStates>
|
</selectionStates>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,13 @@ dependencies {
|
|||||||
implementation group: 'org.mapsforge', name: 'mapsforge-map-reader', version: '0.25.0'
|
implementation group: 'org.mapsforge', name: 'mapsforge-map-reader', version: '0.25.0'
|
||||||
implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.25.0'
|
implementation group: 'org.mapsforge', name: 'mapsforge-core', version: '0.25.0'
|
||||||
|
|
||||||
|
// Room
|
||||||
|
implementation "androidx.room:room-runtime:2.6.1"
|
||||||
|
annotationProcessor "androidx.room:room-compiler:2.6.1"
|
||||||
|
// Lifecycle (для сервисов/репозитория при необходимости)
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-runtime:2.8.3'
|
||||||
|
implementation 'androidx.lifecycle:lifecycle-livedata:2.8.3'
|
||||||
|
|
||||||
// Тестирование
|
// Тестирование
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
<!-- Разрешения для GPS -->
|
<!-- Разрешения для GPS -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
<!-- Для работы в фоне (Android 10+) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<!-- Для ForegroundService с локацией (Android 9+) -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
|
||||||
<!-- Разрешения для интернета (для Яндекс.Карт) -->
|
<!-- Разрешения для интернета (для Яндекс.Карт) -->
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
@@ -13,6 +18,9 @@
|
|||||||
<!-- Разрешения для UDP -->
|
<!-- Разрешения для UDP -->
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
|
|
||||||
|
<!-- Разрешения для вибрации -->
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
<!-- Разрешения для записи в файл (для логирования) -->
|
<!-- Разрешения для записи в файл (для логирования) -->
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28" />
|
android:maxSdkVersion="28" />
|
||||||
@@ -31,7 +39,7 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.AISMap"
|
android:theme="@style/Theme.AISMap"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<profileable android:shell="true" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -49,6 +57,18 @@
|
|||||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
android:theme="@style/Theme.AISMap" />
|
android:theme="@style/Theme.AISMap" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".AisTargetsActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:theme="@style/Theme.AISMap" />
|
||||||
|
|
||||||
|
<!-- Foreground Service для фоновых обновлений AIS/GPS -->
|
||||||
|
<service
|
||||||
|
android:name=".services.AISForegroundService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="location" />
|
||||||
|
|
||||||
<!-- Мета-данные для Яндекс.Карт -->
|
<!-- Мета-данные для Яндекс.Карт -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.yandex.mapkit.ApiKey"
|
android:name="com.yandex.mapkit.ApiKey"
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.grigowashere.aismap;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.Repository;
|
||||||
|
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AisTargetsActivity extends AppCompatActivity implements AisTargetsAdapter.OnItemClickListener {
|
||||||
|
|
||||||
|
private Repository repository;
|
||||||
|
private RecyclerView recyclerView;
|
||||||
|
private AisTargetsAdapter adapter;
|
||||||
|
private android.os.Handler tickerHandler;
|
||||||
|
private Runnable tickerRunnable;
|
||||||
|
private android.widget.TextView textEmptyState;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_ais_targets);
|
||||||
|
|
||||||
|
repository = new Repository(this);
|
||||||
|
|
||||||
|
recyclerView = findViewById(R.id.recycler_ais_targets);
|
||||||
|
textEmptyState = findViewById(R.id.text_empty_state);
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
adapter = new AisTargetsAdapter(new ArrayList<>(), this);
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
repository.observeAllAIS().observe(this, new Observer<List<AISVesselEntity>>() {
|
||||||
|
@Override
|
||||||
|
public void onChanged(List<AISVesselEntity> entities) {
|
||||||
|
// Стабильная сортировка по MMSI для предсказуемого порядка
|
||||||
|
if (entities != null) {
|
||||||
|
java.util.Collections.sort(entities, (a, b) -> a.mmsi.compareTo(b.mmsi));
|
||||||
|
}
|
||||||
|
adapter.submitList(entities);
|
||||||
|
|
||||||
|
// Показываем/скрываем сообщение о пустом состоянии
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Тикер для обновления поля "N сек назад"
|
||||||
|
tickerHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||||
|
tickerRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
// Обновляем только элементы с данными, чтобы избежать мигания
|
||||||
|
int itemCount = adapter.getItemCount();
|
||||||
|
for (int i = 0; i < itemCount; i++) {
|
||||||
|
adapter.notifyItemChanged(i, "time_update");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
tickerHandler.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tickerHandler.postDelayed(tickerRunnable, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMarinetrafficClick(String mmsi) {
|
||||||
|
String url = "https://www.marinetraffic.com/ru/ais/details/ships/mmsi:" + mmsi;
|
||||||
|
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||||
|
startActivity(browserIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCenterOnMapClick(String mmsi, double lat, double lon) {
|
||||||
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||||
|
intent.putExtra("center_mmsi", mmsi);
|
||||||
|
intent.putExtra("center_lat", lat);
|
||||||
|
intent.putExtra("center_lon", lon);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (tickerHandler != null && tickerRunnable != null) {
|
||||||
|
tickerHandler.removeCallbacks(tickerRunnable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package com.grigowashere.aismap;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||||
|
|
||||||
|
class AisTargetsAdapter extends ListAdapter<AISVesselEntity, AisTargetsAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
interface OnItemClickListener {
|
||||||
|
void onMarinetrafficClick(String mmsi);
|
||||||
|
void onCenterOnMapClick(String mmsi, double lat, double lon);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final OnItemClickListener listener;
|
||||||
|
|
||||||
|
protected AisTargetsAdapter(@NonNull DiffUtil.ItemCallback<AISVesselEntity> diffCallback, OnItemClickListener listener) {
|
||||||
|
super(diffCallback);
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AisTargetsAdapter(java.util.List<AISVesselEntity> initial, OnItemClickListener listener) {
|
||||||
|
this(DIFF_CALLBACK, listener);
|
||||||
|
submitList(initial);
|
||||||
|
}
|
||||||
|
|
||||||
|
static final DiffUtil.ItemCallback<AISVesselEntity> DIFF_CALLBACK = new DiffUtil.ItemCallback<AISVesselEntity>() {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull AISVesselEntity oldItem, @NonNull AISVesselEntity newItem) {
|
||||||
|
return oldItem.mmsi.equals(newItem.mmsi);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull AISVesselEntity oldItem, @NonNull AISVesselEntity newItem) {
|
||||||
|
return oldItem.latitude == newItem.latitude &&
|
||||||
|
oldItem.longitude == newItem.longitude &&
|
||||||
|
oldItem.course == newItem.course &&
|
||||||
|
oldItem.speed == newItem.speed &&
|
||||||
|
((oldItem.vesselName == null && newItem.vesselName == null) || (oldItem.vesselName != null && oldItem.vesselName.equals(newItem.vesselName)));
|
||||||
|
// Не проверяем lastUpdateEpochMs, чтобы избежать мигания при обновлении времени
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_ais_target, parent, false);
|
||||||
|
return new ViewHolder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
|
AISVesselEntity item = getItem(position);
|
||||||
|
holder.bind(item, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull java.util.List<Object> payloads) {
|
||||||
|
if (payloads.isEmpty()) {
|
||||||
|
super.onBindViewHolder(holder, position, payloads);
|
||||||
|
} else {
|
||||||
|
// Частичное обновление только времени
|
||||||
|
AISVesselEntity item = getItem(position);
|
||||||
|
holder.updateTimeOnly(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
TextView tvTitle;
|
||||||
|
TextView tvMmsi;
|
||||||
|
TextView tvCoords;
|
||||||
|
TextView tvCourseSpeed;
|
||||||
|
TextView tvLastUpdate;
|
||||||
|
TextView tvTimeAgo;
|
||||||
|
Button btnMarineTraffic;
|
||||||
|
Button btnCenterOnMap;
|
||||||
|
|
||||||
|
ViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
tvTitle = itemView.findViewById(R.id.tv_title);
|
||||||
|
tvMmsi = itemView.findViewById(R.id.tv_mmsi);
|
||||||
|
tvCoords = itemView.findViewById(R.id.tv_coords);
|
||||||
|
tvCourseSpeed = itemView.findViewById(R.id.tv_course_speed);
|
||||||
|
tvLastUpdate = itemView.findViewById(R.id.tv_last_update);
|
||||||
|
tvTimeAgo = itemView.findViewById(R.id.tv_time_ago);
|
||||||
|
btnMarineTraffic = itemView.findViewById(R.id.btn_marine_traffic);
|
||||||
|
btnCenterOnMap = itemView.findViewById(R.id.btn_center_on_map);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(AISVesselEntity entity, OnItemClickListener listener) {
|
||||||
|
String name = entity.vesselName != null && !entity.vesselName.isEmpty() ? entity.vesselName : "MMSI " + entity.mmsi;
|
||||||
|
tvTitle.setText(name);
|
||||||
|
tvMmsi.setText("MMSI: " + entity.mmsi);
|
||||||
|
tvCoords.setText(String.format(java.util.Locale.getDefault(), "%.6f, %.6f", entity.latitude, entity.longitude));
|
||||||
|
tvCourseSpeed.setText(String.format(java.util.Locale.getDefault(), "COG %.1f° • %.1f kn", entity.course, entity.speed));
|
||||||
|
// Время последнего обновления и ago
|
||||||
|
if (entity.lastUpdateEpochMs > 0) {
|
||||||
|
java.text.SimpleDateFormat df = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss", java.util.Locale.getDefault());
|
||||||
|
String last = df.format(new java.util.Date(entity.lastUpdateEpochMs));
|
||||||
|
tvLastUpdate.setText("Обновлено: " + last);
|
||||||
|
long secondsAgo = (System.currentTimeMillis() - entity.lastUpdateEpochMs) / 1000L;
|
||||||
|
tvTimeAgo.setText("" + secondsAgo + " сек назад");
|
||||||
|
} else {
|
||||||
|
tvLastUpdate.setText("Обновлено: --");
|
||||||
|
tvTimeAgo.setText("-- сек назад");
|
||||||
|
}
|
||||||
|
btnMarineTraffic.setOnClickListener(v -> listener.onMarinetrafficClick(entity.mmsi));
|
||||||
|
btnCenterOnMap.setOnClickListener(v -> {
|
||||||
|
android.util.Log.i("AisTargetsAdapter", "Кнопка 'На карте' нажата для MMSI=" + entity.mmsi + ", lat=" + entity.latitude + ", lon=" + entity.longitude);
|
||||||
|
listener.onCenterOnMapClick(entity.mmsi, entity.latitude, entity.longitude);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTimeOnly(AISVesselEntity entity) {
|
||||||
|
// Обновляем только поля времени, чтобы избежать мигания всего элемента
|
||||||
|
if (entity.lastUpdateEpochMs > 0) {
|
||||||
|
java.text.SimpleDateFormat df = new java.text.SimpleDateFormat("dd.MM.yyyy HH:mm:ss", java.util.Locale.getDefault());
|
||||||
|
String last = df.format(new java.util.Date(entity.lastUpdateEpochMs));
|
||||||
|
tvLastUpdate.setText("Обновлено: " + last);
|
||||||
|
long secondsAgo = (System.currentTimeMillis() - entity.lastUpdateEpochMs) / 1000L;
|
||||||
|
tvTimeAgo.setText("" + secondsAgo + " сек назад");
|
||||||
|
} else {
|
||||||
|
tvLastUpdate.setText("Обновлено: --");
|
||||||
|
tvTimeAgo.setText("-- сек назад");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ import android.widget.LinearLayout;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.graphics.Color;
|
||||||
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
@@ -31,6 +32,7 @@ import com.grigowashere.aismap.view.CoordinatesDockWidget;
|
|||||||
import com.grigowashere.aismap.view.BaseDockWidget;
|
import com.grigowashere.aismap.view.BaseDockWidget;
|
||||||
import com.grigowashere.aismap.utils.SettingsManager;
|
import com.grigowashere.aismap.utils.SettingsManager;
|
||||||
import com.grigowashere.aismap.utils.LogSender;
|
import com.grigowashere.aismap.utils.LogSender;
|
||||||
|
import com.grigowashere.aismap.utils.MIDToCountry;
|
||||||
import com.yandex.mapkit.mapview.MapView;
|
import com.yandex.mapkit.mapview.MapView;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -53,10 +55,15 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private Button btnCenterOnVessel;
|
private Button btnCenterOnVessel;
|
||||||
private Button btnMapOrientation;
|
private Button btnMapOrientation;
|
||||||
private Button btnSettings;
|
private Button btnSettings;
|
||||||
|
private Button btnAisTargets;
|
||||||
private LinearLayout controlPanel;
|
private LinearLayout controlPanel;
|
||||||
private CompassView compassView;
|
private CompassView compassView;
|
||||||
private CompassSensor compassSensor;
|
private CompassSensor compassSensor;
|
||||||
private CoordinatesDockWidget coordinatesWidget;
|
private CoordinatesDockWidget coordinatesWidget;
|
||||||
|
private TextView tvGpsAge;
|
||||||
|
private TextView tvAisAge;
|
||||||
|
private android.os.Handler messageAgeHandler;
|
||||||
|
private Runnable messageAgeRunnable;
|
||||||
|
|
||||||
// BottomSheet для отображения информации о нашем судне
|
// BottomSheet для отображения информации о нашем судне
|
||||||
private BottomSheetDialog ownVesselBottomSheet;
|
private BottomSheetDialog ownVesselBottomSheet;
|
||||||
@@ -74,6 +81,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
private Runnable bottomSheetUpdateRunnable; // Runnable для обновления BottomSheet
|
private Runnable bottomSheetUpdateRunnable; // Runnable для обновления BottomSheet
|
||||||
private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000; // Обновление каждую секунду
|
private static final int BOTTOM_SHEET_UPDATE_INTERVAL = 1000; // Обновление каждую секунду
|
||||||
|
|
||||||
|
// Отложенное центрирование из внешнего интента
|
||||||
|
private Double pendingCenterLat = null;
|
||||||
|
private Double pendingCenterLon = null;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -93,9 +104,12 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
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);
|
||||||
btnSettings = findViewById(R.id.btn_settings);
|
btnSettings = findViewById(R.id.btn_settings);
|
||||||
|
btnAisTargets = findViewById(R.id.btn_ais_targets);
|
||||||
controlPanel = findViewById(R.id.control_panel);
|
controlPanel = findViewById(R.id.control_panel);
|
||||||
compassView = findViewById(R.id.compass_view);
|
compassView = findViewById(R.id.compass_view);
|
||||||
coordinatesWidget = findViewById(R.id.coordinates_widget);
|
coordinatesWidget = findViewById(R.id.coordinates_widget);
|
||||||
|
tvGpsAge = findViewById(R.id.tv_gps_age);
|
||||||
|
tvAisAge = findViewById(R.id.tv_ais_age);
|
||||||
|
|
||||||
// Инициализируем магнитный компас
|
// Инициализируем магнитный компас
|
||||||
compassSensor = new CompassSensor(this);
|
compassSensor = new CompassSensor(this);
|
||||||
@@ -104,12 +118,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
setupButtonListeners();
|
setupButtonListeners();
|
||||||
setupCompass();
|
setupCompass();
|
||||||
setupCoordinatesWidget();
|
setupCoordinatesWidget();
|
||||||
|
setupMessageAgesUpdater();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupButtonListeners() {
|
private void setupButtonListeners() {
|
||||||
btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
|
btnCenterOnVessel.setOnClickListener(v -> centerOnVessel());
|
||||||
btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
|
btnMapOrientation.setOnClickListener(v -> toggleMapOrientation());
|
||||||
btnSettings.setOnClickListener(v -> showSettings());
|
btnSettings.setOnClickListener(v -> showSettings());
|
||||||
|
if (btnAisTargets != null) {
|
||||||
|
btnAisTargets.setOnClickListener(v -> openAisTargets());
|
||||||
|
}
|
||||||
|
|
||||||
// Кнопка для показа информации о судне
|
// Кнопка для показа информации о судне
|
||||||
// Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info);
|
// Button btnShowVesselInfo = findViewById(R.id.btn_show_vessel_info);
|
||||||
@@ -217,6 +235,46 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupMessageAgesUpdater() {
|
||||||
|
messageAgeHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||||
|
messageAgeRunnable = new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
if (appController != null) {
|
||||||
|
int gpsSec = appController.getSecondsSinceLastGPSMessage();
|
||||||
|
int aisSec = appController.getSecondsSinceLastAISMessage();
|
||||||
|
if (tvGpsAge != null) {
|
||||||
|
tvGpsAge.setText(gpsSec >= 0 ? ("GPS: " + gpsSec + " сек назад") : "GPS: --");
|
||||||
|
tvGpsAge.setTextColor(getAgeColor(gpsSec));
|
||||||
|
}
|
||||||
|
if (tvAisAge != null) {
|
||||||
|
tvAisAge.setText(aisSec >= 0 ? ("AIS: " + aisSec + " сек назад") : "AIS: --");
|
||||||
|
tvAisAge.setTextColor(getAgeColor(aisSec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
messageAgeHandler.postDelayed(this, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// Стартуем после первичной инициализации
|
||||||
|
messageAgeHandler.postDelayed(messageAgeRunnable, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getAgeColor(int seconds) {
|
||||||
|
if (seconds < 0) {
|
||||||
|
// Нет данных
|
||||||
|
return Color.parseColor("#F44336"); // красный
|
||||||
|
}
|
||||||
|
if (seconds < 30) {
|
||||||
|
return Color.parseColor("#4CAF50"); // зелёный
|
||||||
|
} else if (seconds < 300) {
|
||||||
|
return Color.parseColor("#FFC107"); // жёлтый
|
||||||
|
} else {
|
||||||
|
return Color.parseColor("#F44336"); // красный
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels) {
|
private void onUpdateCompass(float azimuth, List<AISVessel> nearbyVessels) {
|
||||||
if (compassView != null) {
|
if (compassView != null) {
|
||||||
compassView.setAzimuth(azimuth);
|
compassView.setAzimuth(azimuth);
|
||||||
@@ -329,6 +387,18 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
mapController = new MapController(this);
|
mapController = new MapController(this);
|
||||||
|
|
||||||
// Устанавливаем callback для обновления UI
|
// Устанавливаем callback для обновления UI
|
||||||
|
|
||||||
|
// Запускаем Foreground Service для фоновых обновлений AIS/GPS
|
||||||
|
try {
|
||||||
|
android.content.Intent svc = new android.content.Intent(this, com.grigowashere.aismap.services.AISForegroundService.class);
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
startForegroundService(svc);
|
||||||
|
} else {
|
||||||
|
startService(svc);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e("MainActivity", "Не удалось запустить ForegroundService: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
appController.setUIUpdateCallback(new AppController.ExtendedUIUpdateCallback() {
|
appController.setUIUpdateCallback(new AppController.ExtendedUIUpdateCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onVesselPositionUpdated(Vessel vessel) {
|
public void onVesselPositionUpdated(Vessel vessel) {
|
||||||
@@ -470,17 +540,42 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, "Переключение ориентации карты (в разработке)", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void togglePathTracking() {
|
||||||
|
boolean currentState = settingsManager.isPathTrackingEnabled();
|
||||||
|
boolean newState = !currentState;
|
||||||
|
|
||||||
|
settingsManager.setPathTrackingEnabled(newState);
|
||||||
|
|
||||||
|
// Обновляем состояние в карте
|
||||||
|
if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) {
|
||||||
|
((com.grigowashere.aismap.maps.YandexMapImpl) mapInterface).setPathTrackingEnabled(newState);
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = newState ? "Отслеживание путей включено" : "Отслеживание путей выключено";
|
||||||
|
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
|
||||||
|
|
||||||
|
// Обновляем меню
|
||||||
|
invalidateOptionsMenu();
|
||||||
|
}
|
||||||
|
|
||||||
private void showSettings() {
|
private void showSettings() {
|
||||||
Intent intent = new Intent(this, SettingsActivity.class);
|
Intent intent = new Intent(this, SettingsActivity.class);
|
||||||
startActivityForResult(intent, SETTINGS_REQUEST_CODE);
|
startActivityForResult(intent, SETTINGS_REQUEST_CODE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void openAisTargets() {
|
||||||
|
Intent intent = new Intent(this, AisTargetsActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обновляет позицию панели управления в зависимости от состояния docked виджетов
|
* Обновляет позицию панели управления в зависимости от состояния docked виджетов
|
||||||
*/
|
*/
|
||||||
private void updateControlPanelPosition() {
|
private void updateControlPanelPosition() {
|
||||||
if (controlPanel != null) {
|
if (controlPanel != null) {
|
||||||
runOnUiThread(() -> {
|
// Используем postDelayed для предотвращения частых обновлений layout
|
||||||
|
controlPanel.postDelayed(() -> {
|
||||||
|
try {
|
||||||
// Получаем текущие параметры layout
|
// Получаем текущие параметры layout
|
||||||
android.widget.RelativeLayout.LayoutParams params =
|
android.widget.RelativeLayout.LayoutParams params =
|
||||||
(android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams();
|
(android.widget.RelativeLayout.LayoutParams) controlPanel.getLayoutParams();
|
||||||
@@ -534,7 +629,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
", compassTop=" + (compassView != null ? compassView.isDockTop() : false) +
|
", compassTop=" + (compassView != null ? compassView.isDockTop() : false) +
|
||||||
", coordinatesDocked=" + (coordinatesWidget != null ? coordinatesWidget.isDocked() : false) +
|
", coordinatesDocked=" + (coordinatesWidget != null ? coordinatesWidget.isDocked() : false) +
|
||||||
", coordinatesTop=" + (coordinatesWidget != null ? coordinatesWidget.isDockTop() : false));
|
", coordinatesTop=" + (coordinatesWidget != null ? coordinatesWidget.isDockTop() : false));
|
||||||
});
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка при обновлении позиции панели управления: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}, 50); // Задержка 50мс для throttling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -575,6 +673,16 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
mapInterface.initialize();
|
mapInterface.initialize();
|
||||||
Log.i(TAG, "Карта инициализирована");
|
Log.i(TAG, "Карта инициализирована");
|
||||||
|
|
||||||
|
// Применяем отложенное центрирование, если было
|
||||||
|
applyPendingCenterIfAny();
|
||||||
|
|
||||||
|
// Инициализируем отслеживание путей
|
||||||
|
if (mapInterface instanceof com.grigowashere.aismap.maps.YandexMapImpl) {
|
||||||
|
boolean pathTrackingEnabled = settingsManager.isPathTrackingEnabled();
|
||||||
|
((com.grigowashere.aismap.maps.YandexMapImpl) mapInterface).setPathTrackingEnabled(pathTrackingEnabled);
|
||||||
|
Log.i(TAG, "Отслеживание путей: " + (pathTrackingEnabled ? "включено" : "выключено"));
|
||||||
|
}
|
||||||
|
|
||||||
// Проверяем, что все настроено правильно
|
// Проверяем, что все настроено правильно
|
||||||
Log.i(TAG, "Проверяем настройку карты...");
|
Log.i(TAG, "Проверяем настройку карты...");
|
||||||
|
|
||||||
@@ -590,10 +698,54 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обрабатываем возможный интент центрирования
|
||||||
|
handleCenterIntentIfAny(getIntent());
|
||||||
|
|
||||||
// Проверяем разрешения и запускаем контроллеры
|
// Проверяем разрешения и запускаем контроллеры
|
||||||
checkPermissions();
|
checkPermissions();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
handleCenterIntentIfAny(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCenterIntentIfAny(Intent intent) {
|
||||||
|
if (intent == null) return;
|
||||||
|
if (intent.hasExtra("center_lat") && intent.hasExtra("center_lon")) {
|
||||||
|
double lat = intent.getDoubleExtra("center_lat", 0);
|
||||||
|
double lon = intent.getDoubleExtra("center_lon", 0);
|
||||||
|
Log.i(TAG, "Получен интент центрирования: lat=" + lat + ", lon=" + lon);
|
||||||
|
if (lat != 0 || lon != 0) {
|
||||||
|
if (mapInterface != null) {
|
||||||
|
Log.i(TAG, "Центрируем карту немедленно");
|
||||||
|
mapInterface.centerOnPosition(lat, lon);
|
||||||
|
} else {
|
||||||
|
// Сохраняем для применения после инициализации карты
|
||||||
|
Log.i(TAG, "Сохраняем координаты для отложенного центрирования");
|
||||||
|
pendingCenterLat = lat;
|
||||||
|
pendingCenterLon = lon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Сбрасываем, чтобы не повторялось при поворотах
|
||||||
|
intent.removeExtra("center_lat");
|
||||||
|
intent.removeExtra("center_lon");
|
||||||
|
intent.removeExtra("center_mmsi");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPendingCenterIfAny() {
|
||||||
|
if (mapInterface == null) return;
|
||||||
|
if (pendingCenterLat != null && pendingCenterLon != null) {
|
||||||
|
Log.i(TAG, "Применяем отложенное центрирование: lat=" + pendingCenterLat + ", lon=" + pendingCenterLon);
|
||||||
|
mapInterface.centerOnPosition(pendingCenterLat, pendingCenterLon);
|
||||||
|
pendingCenterLat = null;
|
||||||
|
pendingCenterLon = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStop() {
|
protected void onStop() {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
@@ -603,10 +755,10 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
mapInterface.cleanup();
|
mapInterface.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Останавливаем все слушатели
|
// Не останавливаем слушатели здесь, чтобы UDP продолжал работать в фоне
|
||||||
if (appController != null) {
|
// if (appController != null) {
|
||||||
appController.stopAllListeners();
|
// appController.stopAllListeners();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -727,6 +879,12 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
udpItem.setTitle(appController.isUDPEnabled() ? "UDP ✓" : "UDP");
|
udpItem.setTitle(appController.isUDPEnabled() ? "UDP ✓" : "UDP");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MenuItem pathItem = menu.findItem(R.id.menu_path_tracking);
|
||||||
|
if (pathItem != null) {
|
||||||
|
boolean pathEnabled = settingsManager.isPathTrackingEnabled();
|
||||||
|
pathItem.setTitle(pathEnabled ? "Пути ✓" : "Пути");
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,6 +901,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
} else if (id == R.id.menu_clear_ais) {
|
} else if (id == R.id.menu_clear_ais) {
|
||||||
clearAIS();
|
clearAIS();
|
||||||
return true;
|
return true;
|
||||||
|
} else if (id == R.id.menu_path_tracking) {
|
||||||
|
togglePathTracking();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
@@ -996,12 +1157,13 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
// Обновляем все поля в AIS BottomSheet
|
// Обновляем все поля в AIS BottomSheet
|
||||||
TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title);
|
TextView tvTitle = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_title);
|
||||||
TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi);
|
TextView tvMmsi = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_mmsi);
|
||||||
TextView tvName = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_name);
|
|
||||||
TextView tvCallsign = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_callsign);
|
TextView tvCallsign = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_callsign);
|
||||||
TextView tvImo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_imo);
|
TextView tvImo = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_imo);
|
||||||
TextView tvType = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_type);
|
TextView tvType = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_type);
|
||||||
TextView tvPosition = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_position);
|
TextView tvPosition = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_position);
|
||||||
TextView tvCourse = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_course);
|
TextView tvCourse = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_course);
|
||||||
|
TextView tvRot = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_rot);
|
||||||
|
TextView tvHeading = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_heading);
|
||||||
TextView tvSpeed = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_speed);
|
TextView tvSpeed = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_speed);
|
||||||
TextView tvDimensions = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_dimensions);
|
TextView tvDimensions = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_dimensions);
|
||||||
TextView tvDraft = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_draft);
|
TextView tvDraft = aisBottomSheetView.findViewById(R.id.bottom_sheet_ais_draft);
|
||||||
@@ -1015,9 +1177,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
// Заголовок
|
// Заголовок
|
||||||
if (tvTitle != null) {
|
if (tvTitle != null) {
|
||||||
String title = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty()
|
String name = vessel.getVesselName() != null && !vessel.getVesselName().isEmpty()
|
||||||
? "🚢 " + vessel.getVesselName()
|
? vessel.getVesselName()
|
||||||
: "🚢 AIS СУДНО";
|
: "AIS СУДНО";
|
||||||
|
String flag = getFlagEmojiForMMSI(vessel.getMmsi());
|
||||||
|
String title = (flag != null ? flag + " " : "") + "🚢 " + name;
|
||||||
tvTitle.setText(title);
|
tvTitle.setText(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1027,9 +1191,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Название судна
|
// Название судна
|
||||||
if (tvName != null) {
|
|
||||||
tvName.setText("📛 Название: " + (vessel.getVesselName() != null ? vessel.getVesselName() : "--"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Позывной
|
// Позывной
|
||||||
if (tvCallsign != null) {
|
if (tvCallsign != null) {
|
||||||
@@ -1057,13 +1219,34 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Курс
|
// Курс (COG)
|
||||||
if (tvCourse != null) {
|
if (tvCourse != null) {
|
||||||
if (vessel.getCourse() > 0) {
|
if (vessel.getCourse() > 0) {
|
||||||
String courseText = String.format("🧭 Курс: %.1f°", vessel.getCourse());
|
String courseText = String.format("🧭 COG: %.1f°", vessel.getCourse());
|
||||||
tvCourse.setText(courseText);
|
tvCourse.setText(courseText);
|
||||||
} else {
|
} else {
|
||||||
tvCourse.setText("🧭 Курс: --°");
|
tvCourse.setText("🧭 COG: --°");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Скорость поворота (ROT)
|
||||||
|
if (tvRot != null) {
|
||||||
|
double rot = vessel.getRateOfTurn();
|
||||||
|
if (rot != 0) {
|
||||||
|
String rotText = String.format("🔄 ROT: %.1f°/мин", rot);
|
||||||
|
tvRot.setText(rotText);
|
||||||
|
} else {
|
||||||
|
tvRot.setText("🔄 ROT: --°/мин");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Направление (HDG)
|
||||||
|
if (tvHeading != null) {
|
||||||
|
if (vessel.getHeading() > 0) {
|
||||||
|
String headingText = String.format("🧭 HDG: %.1f°", vessel.getHeading());
|
||||||
|
tvHeading.setText(headingText);
|
||||||
|
} else {
|
||||||
|
tvHeading.setText("🧭 HDG: --°");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1130,7 +1313,9 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
String signalText = String.format("📶 Сигнал: %d", vessel.getSignalStrength());
|
String signalText = String.format("📶 Сигнал: %d", vessel.getSignalStrength());
|
||||||
tvSignal.setText(signalText);
|
tvSignal.setText(signalText);
|
||||||
} else {
|
} else {
|
||||||
tvSignal.setText("📶 Сигнал: --");
|
// Показываем качество позиции по AIS Accuracy биту
|
||||||
|
String qualityText = vessel.isPositionAccuracy() ? "📶 Точность: высокая" : "📶 Точность: низкая";
|
||||||
|
tvSignal.setText(qualityText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1176,6 +1361,26 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает флаг-эмодзи по MMSI через MID->ISO2.
|
||||||
|
*/
|
||||||
|
private String getFlagEmojiForMMSI(String mmsi) {
|
||||||
|
try {
|
||||||
|
if (mmsi == null || mmsi.length() < 3) return null;
|
||||||
|
String mid = mmsi.substring(0, 3);
|
||||||
|
String iso2 = MIDToCountry.MID_TO_COUNTRY.get(mid);
|
||||||
|
if (iso2 == null || iso2.length() != 2) return null;
|
||||||
|
char a = Character.toUpperCase(iso2.charAt(0));
|
||||||
|
char b = Character.toUpperCase(iso2.charAt(1));
|
||||||
|
int base = 0x1F1E6;
|
||||||
|
int cp1 = base + (a - 'A');
|
||||||
|
int cp2 = base + (b - 'A');
|
||||||
|
return new String(Character.toChars(cp1)) + new String(Character.toChars(cp2));
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Восстанавливает обработчики кликов для маркеров
|
* Восстанавливает обработчики кликов для маркеров
|
||||||
*/
|
*/
|
||||||
@@ -1226,7 +1431,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
if (!isYandexMapsInitialized) {
|
if (!isYandexMapsInitialized) {
|
||||||
try {
|
try {
|
||||||
// Инициализация Яндекс.Карт
|
// Инициализация Яндекс.Карт
|
||||||
com.yandex.mapkit.MapKitFactory.setApiKey("your_api_key_here");
|
com.yandex.mapkit.MapKitFactory.setApiKey("9ae1917c-2049-4927-9d1e-29dd0d3e8ebc");
|
||||||
com.yandex.mapkit.MapKitFactory.initialize(this);
|
com.yandex.mapkit.MapKitFactory.initialize(this);
|
||||||
isYandexMapsInitialized = true;
|
isYandexMapsInitialized = true;
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
private RadioButton radioHybridMode;
|
private RadioButton radioHybridMode;
|
||||||
private RadioButton radioNMEAOnly;
|
private RadioButton radioNMEAOnly;
|
||||||
private RadioButton radioAndroidOnly;
|
private RadioButton radioAndroidOnly;
|
||||||
|
private EditText etStaleWarningMinutes;
|
||||||
|
private EditText etStaleRemoveMinutes;
|
||||||
|
private SwitchMaterial switchVibrationEnabled;
|
||||||
|
private SwitchMaterial switchSoundEnabled;
|
||||||
private Button btnCancel;
|
private Button btnCancel;
|
||||||
private Button btnSave;
|
private Button btnSave;
|
||||||
|
|
||||||
@@ -42,6 +46,10 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
private boolean originalAndroidNMEAEnabled;
|
private boolean originalAndroidNMEAEnabled;
|
||||||
private boolean originalUDPNMEAEnabled;
|
private boolean originalUDPNMEAEnabled;
|
||||||
private String originalDataMode;
|
private String originalDataMode;
|
||||||
|
private int originalStaleWarningMinutes;
|
||||||
|
private int originalStaleRemoveMinutes;
|
||||||
|
private boolean originalVibrationEnabled;
|
||||||
|
private boolean originalSoundEnabled;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -78,6 +86,10 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
radioHybridMode = findViewById(R.id.radio_hybrid_mode);
|
radioHybridMode = findViewById(R.id.radio_hybrid_mode);
|
||||||
radioNMEAOnly = findViewById(R.id.radio_nmea_only);
|
radioNMEAOnly = findViewById(R.id.radio_nmea_only);
|
||||||
radioAndroidOnly = findViewById(R.id.radio_android_only);
|
radioAndroidOnly = findViewById(R.id.radio_android_only);
|
||||||
|
etStaleWarningMinutes = findViewById(R.id.et_stale_warning_minutes);
|
||||||
|
etStaleRemoveMinutes = findViewById(R.id.et_stale_remove_minutes);
|
||||||
|
switchVibrationEnabled = findViewById(R.id.switch_vibration_enabled);
|
||||||
|
switchSoundEnabled = findViewById(R.id.switch_sound_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);
|
||||||
}
|
}
|
||||||
@@ -108,6 +120,14 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Настройки устаревания данных
|
||||||
|
etStaleWarningMinutes.setText(String.valueOf(settingsManager.getDataStaleWarningMinutes()));
|
||||||
|
etStaleRemoveMinutes.setText(String.valueOf(settingsManager.getDataStaleRemoveMinutes()));
|
||||||
|
|
||||||
|
// Настройки уведомлений
|
||||||
|
switchVibrationEnabled.setChecked(settingsManager.isVibrationEnabled());
|
||||||
|
switchSoundEnabled.setChecked(settingsManager.isSoundEnabled());
|
||||||
|
|
||||||
Log.i(TAG, "Настройки загружены в UI");
|
Log.i(TAG, "Настройки загружены в UI");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +140,10 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
originalAndroidNMEAEnabled = settingsManager.isAndroidNMEAEnabled();
|
originalAndroidNMEAEnabled = settingsManager.isAndroidNMEAEnabled();
|
||||||
originalUDPNMEAEnabled = settingsManager.isUDPNMEAEnabled();
|
originalUDPNMEAEnabled = settingsManager.isUDPNMEAEnabled();
|
||||||
originalDataMode = settingsManager.getDataMode();
|
originalDataMode = settingsManager.getDataMode();
|
||||||
|
originalStaleWarningMinutes = settingsManager.getDataStaleWarningMinutes();
|
||||||
|
originalStaleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes();
|
||||||
|
originalVibrationEnabled = settingsManager.isVibrationEnabled();
|
||||||
|
originalSoundEnabled = settingsManager.isSoundEnabled();
|
||||||
|
|
||||||
Log.i(TAG, "Оригинальные настройки сохранены");
|
Log.i(TAG, "Оригинальные настройки сохранены");
|
||||||
}
|
}
|
||||||
@@ -221,12 +245,29 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Валидируем настройки устаревания данных
|
||||||
|
int staleWarningMinutes = validateStaleMinutes(etStaleWarningMinutes.getText().toString().trim(), "время предупреждения");
|
||||||
|
if (staleWarningMinutes == -1) return;
|
||||||
|
|
||||||
|
int staleRemoveMinutes = validateStaleMinutes(etStaleRemoveMinutes.getText().toString().trim(), "время удаления");
|
||||||
|
if (staleRemoveMinutes == -1) return;
|
||||||
|
|
||||||
|
// Проверяем логичность значений
|
||||||
|
if (staleWarningMinutes >= staleRemoveMinutes) {
|
||||||
|
Toast.makeText(this, "Время предупреждения должно быть меньше времени удаления", Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Сохраняем настройки
|
// Сохраняем настройки
|
||||||
settingsManager.setUDPPort(udpPort);
|
settingsManager.setUDPPort(udpPort);
|
||||||
settingsManager.setUDPEnabled(switchUDPEnabled.isChecked());
|
settingsManager.setUDPEnabled(switchUDPEnabled.isChecked());
|
||||||
settingsManager.setAndroidNMEAEnabled(switchAndroidNMEAEnabled.isChecked());
|
settingsManager.setAndroidNMEAEnabled(switchAndroidNMEAEnabled.isChecked());
|
||||||
settingsManager.setUDPNMEAEnabled(switchUDPNMEAEnabled.isChecked());
|
settingsManager.setUDPNMEAEnabled(switchUDPNMEAEnabled.isChecked());
|
||||||
settingsManager.setDataMode(dataMode);
|
settingsManager.setDataMode(dataMode);
|
||||||
|
settingsManager.setDataStaleWarningMinutes(staleWarningMinutes);
|
||||||
|
settingsManager.setDataStaleRemoveMinutes(staleRemoveMinutes);
|
||||||
|
settingsManager.setVibrationEnabled(switchVibrationEnabled.isChecked());
|
||||||
|
settingsManager.setSoundEnabled(switchSoundEnabled.isChecked());
|
||||||
|
|
||||||
Log.i(TAG, "Настройки сохранены: " + settingsManager.getSettingsSummary());
|
Log.i(TAG, "Настройки сохранены: " + settingsManager.getSettingsSummary());
|
||||||
|
|
||||||
@@ -307,6 +348,28 @@ public class SettingsActivity extends AppCompatActivity {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Валидирует время устаревания данных
|
||||||
|
*/
|
||||||
|
private int validateStaleMinutes(String text, String fieldName) {
|
||||||
|
if (text.isEmpty()) {
|
||||||
|
Toast.makeText(this, fieldName + " не может быть пустым", Toast.LENGTH_SHORT).show();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
int minutes = Integer.parseInt(text);
|
||||||
|
if (minutes < 1 || minutes > 60) {
|
||||||
|
Toast.makeText(this, fieldName + " должно быть от 1 до 60 минут", Toast.LENGTH_SHORT).show();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return minutes;
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
Toast.makeText(this, "Некорректный формат " + fieldName, Toast.LENGTH_SHORT).show();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, нужно ли перезапустить сервисы
|
* Проверяет, нужно ли перезапустить сервисы
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ import android.util.Log;
|
|||||||
import com.grigowashere.aismap.models.Vessel;
|
import com.grigowashere.aismap.models.Vessel;
|
||||||
import com.grigowashere.aismap.models.AISVessel;
|
import com.grigowashere.aismap.models.AISVessel;
|
||||||
import com.grigowashere.aismap.maps.MapInterface;
|
import com.grigowashere.aismap.maps.MapInterface;
|
||||||
|
import com.grigowashere.aismap.data.Repository;
|
||||||
|
import com.grigowashere.aismap.data.mapper.AISVesselMapper;
|
||||||
|
import com.grigowashere.aismap.services.NotificationService;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
@@ -34,6 +37,8 @@ public class AppController implements
|
|||||||
private Vessel ownVessel;
|
private Vessel ownVessel;
|
||||||
private List<AISVessel> aisVessels;
|
private List<AISVessel> aisVessels;
|
||||||
private ExecutorService executor;
|
private ExecutorService executor;
|
||||||
|
private com.grigowashere.aismap.data.Repository repository;
|
||||||
|
private NotificationService notificationService;
|
||||||
|
|
||||||
private boolean isUDPEnabled;
|
private boolean isUDPEnabled;
|
||||||
private boolean isAndroidNMEAEnabled;
|
private boolean isAndroidNMEAEnabled;
|
||||||
@@ -42,6 +47,15 @@ public class AppController implements
|
|||||||
private int udpPort;
|
private int udpPort;
|
||||||
private String dataMode;
|
private String dataMode;
|
||||||
|
|
||||||
|
// Время последнего получения сообщений ($ GPS) и (! AIS) в elapsedRealtime
|
||||||
|
private long lastGPSMessageRealtimeMs;
|
||||||
|
private long lastAISMessageRealtimeMs;
|
||||||
|
|
||||||
|
// Периодическая очистка БД от устаревших AIS целей
|
||||||
|
private android.os.Handler dbCleanupHandler;
|
||||||
|
private Runnable dbCleanupRunnable;
|
||||||
|
private static final long DB_CLEANUP_INTERVAL = 60000; // 1 минута
|
||||||
|
|
||||||
// Callback для обновления UI
|
// Callback для обновления UI
|
||||||
private UIUpdateCallback uiUpdateCallback;
|
private UIUpdateCallback uiUpdateCallback;
|
||||||
|
|
||||||
@@ -64,6 +78,12 @@ public class AppController implements
|
|||||||
this.ownVessel = new Vessel();
|
this.ownVessel = new Vessel();
|
||||||
this.aisVessels = new ArrayList<>();
|
this.aisVessels = new ArrayList<>();
|
||||||
this.executor = Executors.newCachedThreadPool();
|
this.executor = Executors.newCachedThreadPool();
|
||||||
|
this.repository = new com.grigowashere.aismap.data.Repository(context);
|
||||||
|
this.notificationService = new NotificationService(context);
|
||||||
|
|
||||||
|
// Инициализируем Handler для периодической очистки БД
|
||||||
|
this.dbCleanupHandler = new android.os.Handler(android.os.Looper.getMainLooper());
|
||||||
|
this.dbCleanupRunnable = this::performDatabaseCleanup;
|
||||||
|
|
||||||
initializeControllers();
|
initializeControllers();
|
||||||
}
|
}
|
||||||
@@ -92,6 +112,29 @@ public class AppController implements
|
|||||||
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
|
// Инициализация Android NMEA слушателя (для курса, скорости, DOP)
|
||||||
androidNmeaListener = new AndroidNMEAListener(context);
|
androidNmeaListener = new AndroidNMEAListener(context);
|
||||||
androidNmeaListener.setCallback(this);
|
androidNmeaListener.setCallback(this);
|
||||||
|
|
||||||
|
// Восстанавливаем данные из БД при старте
|
||||||
|
try {
|
||||||
|
com.grigowashere.aismap.data.entity.VesselEntity latest = repository.getLatestOwnVesselSync();
|
||||||
|
if (latest != null) {
|
||||||
|
ownVessel.setLatitude(latest.latitude);
|
||||||
|
ownVessel.setLongitude(latest.longitude);
|
||||||
|
ownVessel.setAccuracy(latest.accuracy);
|
||||||
|
ownVessel.setFixTime(latest.fixTime);
|
||||||
|
}
|
||||||
|
java.util.List<com.grigowashere.aismap.data.entity.AISVesselEntity> list = repository.getAllAISSync();
|
||||||
|
if (list != null) {
|
||||||
|
for (com.grigowashere.aismap.data.entity.AISVesselEntity entity : list) {
|
||||||
|
// Используем маппер для полного восстановления всех полей
|
||||||
|
AISVessel vessel = AISVesselMapper.toModel(entity);
|
||||||
|
aisVessels.add(vessel);
|
||||||
|
Log.d(TAG, "AIS судно восстановлено из БД с полными данными: " + vessel.getMmsi());
|
||||||
|
}
|
||||||
|
Log.i(TAG, "Восстановлено " + list.size() + " AIS судов из БД с полными данными");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка восстановления данных из БД: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +147,24 @@ public class AppController implements
|
|||||||
Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface");
|
Log.i(TAG, "Устанавливаем MarkerClickListener в MapInterface");
|
||||||
mapInterface.setMarkerClickListener(this);
|
mapInterface.setMarkerClickListener(this);
|
||||||
Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры");
|
Log.i(TAG, "MarkerClickListener установлен, теперь можно создавать маркеры");
|
||||||
|
|
||||||
|
// Восстановление отрисовки сохранённых данных на карте
|
||||||
|
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||||
|
try {
|
||||||
|
// Позиция нашего судна
|
||||||
|
if (ownVessel != null && ownVessel.getLatitude() != 0 && ownVessel.getLongitude() != 0) {
|
||||||
|
mapInterface.updateOwnVesselPosition(ownVessel);
|
||||||
|
}
|
||||||
|
// AIS маркеры
|
||||||
|
if (aisVessels != null && !aisVessels.isEmpty()) {
|
||||||
|
for (AISVessel v : aisVessels) {
|
||||||
|
mapInterface.addAISVesselMarker(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка восстановления отрисовки на карте: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +196,8 @@ public class AppController implements
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Запускаем периодическую очистку БД от устаревших AIS целей
|
||||||
|
startDatabaseCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -144,6 +206,9 @@ public class AppController implements
|
|||||||
* Останавливает все слушатели
|
* Останавливает все слушатели
|
||||||
*/
|
*/
|
||||||
public void stopAllListeners() {
|
public void stopAllListeners() {
|
||||||
|
// Останавливаем периодическую очистку БД
|
||||||
|
stopDatabaseCleanup();
|
||||||
|
|
||||||
executor.execute(() -> {
|
executor.execute(() -> {
|
||||||
udpListener.stop();
|
udpListener.stop();
|
||||||
androidNmeaListener.stopListening();
|
androidNmeaListener.stopListening();
|
||||||
@@ -279,15 +344,28 @@ public class AppController implements
|
|||||||
ownVessel.setFixTime(vessel.getFixTime());
|
ownVessel.setFixTime(vessel.getFixTime());
|
||||||
ownVessel.setFixQuality(vessel.getFixQuality());
|
ownVessel.setFixQuality(vessel.getFixQuality());
|
||||||
|
|
||||||
|
// Сохраняем позицию в локальную БД
|
||||||
|
try {
|
||||||
|
com.grigowashere.aismap.data.entity.VesselEntity ve = new com.grigowashere.aismap.data.entity.VesselEntity();
|
||||||
|
ve.latitude = ownVessel.getLatitude();
|
||||||
|
ve.longitude = ownVessel.getLongitude();
|
||||||
|
ve.accuracy = ownVessel.getAccuracy();
|
||||||
|
ve.fixTime = ownVessel.getFixTime();
|
||||||
|
repository.upsertOwnVessel(ve);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка сохранения позиции в БД: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
// Обновляем UI через callback
|
// Обновляем UI через callback
|
||||||
if (uiUpdateCallback != null) {
|
if (uiUpdateCallback != null) {
|
||||||
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
|
uiUpdateCallback.onVesselPositionUpdated(ownVessel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновляем карту в главном потоке
|
// Обновляем карту в главном потоке с throttling
|
||||||
if (mapInterface != null) {
|
if (mapInterface != null) {
|
||||||
Log.i(TAG, "Обновляем позицию на карте...");
|
Log.i(TAG, "Обновляем позицию на карте...");
|
||||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
// Используем postDelayed для предотвращения частых обновлений
|
||||||
|
new android.os.Handler(android.os.Looper.getMainLooper()).postDelayed(() -> {
|
||||||
try {
|
try {
|
||||||
Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition...");
|
Log.i(TAG, "Вызываем mapInterface.updateOwnVesselPosition...");
|
||||||
mapInterface.updateOwnVesselPosition(ownVessel);
|
mapInterface.updateOwnVesselPosition(ownVessel);
|
||||||
@@ -295,7 +373,7 @@ public class AppController implements
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Log.e(TAG, "Ошибка обновления позиции на карте: " + e.getMessage(), e);
|
Log.e(TAG, "Ошибка обновления позиции на карте: " + e.getMessage(), e);
|
||||||
}
|
}
|
||||||
});
|
}, 100); // Задержка 100мс для throttling
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,6 +459,17 @@ public class AppController implements
|
|||||||
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
|
AISVessel existingVessel = findAISVesselByMMSI(vessel.getMmsi());
|
||||||
|
|
||||||
if (existingVessel != null) {
|
if (existingVessel != null) {
|
||||||
|
// Если пришло новое safety-сообщение (тип 14), уведомим пользователя
|
||||||
|
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
|
||||||
|
String prev = existingVessel.getLastSafetyMessage();
|
||||||
|
String curr = vessel.getLastSafetyMessage();
|
||||||
|
if (prev == null || !prev.equals(curr)) {
|
||||||
|
if (notificationService != null && notificationService.areNotificationsEnabled()) {
|
||||||
|
notificationService.notifySafetyMessage(vessel.getMmsi(), curr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingVessel.setLastSafetyMessage(curr);
|
||||||
|
}
|
||||||
// Обновляем существующее судно
|
// Обновляем существующее судно
|
||||||
existingVessel.updatePosition(
|
existingVessel.updatePosition(
|
||||||
vessel.getLatitude(),
|
vessel.getLatitude(),
|
||||||
@@ -388,6 +477,14 @@ public class AppController implements
|
|||||||
vessel.getCourse(),
|
vessel.getCourse(),
|
||||||
vessel.getSpeed()
|
vessel.getSpeed()
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
// Используем маппер для полной конвертации всех полей
|
||||||
|
com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(existingVessel);
|
||||||
|
repository.upsertAIS(entity);
|
||||||
|
Log.d(TAG, "AIS судно сохранено в БД с полными данными: " + existingVessel.getMmsi());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
if (mapInterface != null) {
|
if (mapInterface != null) {
|
||||||
// Используем Handler для выполнения в главном потоке
|
// Используем Handler для выполнения в главном потоке
|
||||||
@@ -403,6 +500,28 @@ public class AppController implements
|
|||||||
// Добавляем новое судно
|
// Добавляем новое судно
|
||||||
aisVessels.add(vessel);
|
aisVessels.add(vessel);
|
||||||
|
|
||||||
|
// Если это новое судно сразу пришло с safety-сообщением — уведомим
|
||||||
|
if (vessel.getLastSafetyMessage() != null && !vessel.getLastSafetyMessage().isEmpty()) {
|
||||||
|
if (notificationService != null && notificationService.areNotificationsEnabled()) {
|
||||||
|
notificationService.notifySafetyMessage(vessel.getMmsi(), vessel.getLastSafetyMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Воспроизводим уведомление о новой цели
|
||||||
|
if (notificationService != null && notificationService.areNotificationsEnabled()) {
|
||||||
|
notificationService.notifyNewAISTarget();
|
||||||
|
Log.i(TAG, "🔔 Уведомление о новой AIS цели: " + vessel.getMmsi());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Используем маппер для полной конвертации всех полей
|
||||||
|
com.grigowashere.aismap.data.entity.AISVesselEntity entity = AISVesselMapper.toEntity(vessel);
|
||||||
|
repository.upsertAIS(entity);
|
||||||
|
Log.d(TAG, "Новое AIS судно сохранено в БД с полными данными: " + vessel.getMmsi());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка апсерта AIS в БД: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
if (mapInterface != null) {
|
if (mapInterface != null) {
|
||||||
// Используем Handler для выполнения в главном потоке
|
// Используем Handler для выполнения в главном потоке
|
||||||
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> {
|
||||||
@@ -465,6 +584,8 @@ public class AppController implements
|
|||||||
|
|
||||||
// Парсим полученные данные как NMEA
|
// Парсим полученные данные как NMEA
|
||||||
nmeaParser.parseNMEA(data);
|
nmeaParser.parseNMEA(data);
|
||||||
|
// Обновляем метки времени по префиксу
|
||||||
|
updateLastMessageAgesFromRaw(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -485,6 +606,18 @@ public class AppController implements
|
|||||||
|
|
||||||
// Парсим полученные данные как NMEA
|
// Парсим полученные данные как NMEA
|
||||||
nmeaParser.parseNMEA(message);
|
nmeaParser.parseNMEA(message);
|
||||||
|
if (message != null) {
|
||||||
|
String trimmed = message.trim();
|
||||||
|
if (!trimmed.isEmpty()) {
|
||||||
|
char c = trimmed.charAt(0);
|
||||||
|
long now = android.os.SystemClock.elapsedRealtime();
|
||||||
|
if (c == '$') {
|
||||||
|
lastGPSMessageRealtimeMs = now;
|
||||||
|
} else if (c == '!') {
|
||||||
|
lastAISMessageRealtimeMs = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Реализация MarkerClickListener
|
// Реализация MarkerClickListener
|
||||||
@@ -577,11 +710,56 @@ public class AppController implements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Запускает периодическую очистку БД от устаревших AIS целей
|
||||||
|
*/
|
||||||
|
public void startDatabaseCleanup() {
|
||||||
|
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
|
||||||
|
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
|
||||||
|
Log.i(TAG, "Запущена периодическая очистка БД от устаревших AIS целей");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Останавливает периодическую очистку БД
|
||||||
|
*/
|
||||||
|
public void stopDatabaseCleanup() {
|
||||||
|
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
|
||||||
|
dbCleanupHandler.removeCallbacks(dbCleanupRunnable);
|
||||||
|
Log.i(TAG, "Остановлена периодическая очистка БД от устаревших AIS целей");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выполняет очистку БД от устаревших AIS целей
|
||||||
|
*/
|
||||||
|
private void performDatabaseCleanup() {
|
||||||
|
try {
|
||||||
|
com.grigowashere.aismap.utils.SettingsManager settingsManager =
|
||||||
|
new com.grigowashere.aismap.utils.SettingsManager(context);
|
||||||
|
|
||||||
|
int staleRemoveMinutes = settingsManager.getDataStaleRemoveMinutes();
|
||||||
|
long thresholdEpochMs = System.currentTimeMillis() - (staleRemoveMinutes * 60 * 1000L);
|
||||||
|
|
||||||
|
repository.deleteStaleAIS(thresholdEpochMs);
|
||||||
|
|
||||||
|
Log.i(TAG, "Выполнена очистка БД от AIS целей старше " + staleRemoveMinutes + " минут");
|
||||||
|
|
||||||
|
// Планируем следующую очистку
|
||||||
|
if (dbCleanupHandler != null && dbCleanupRunnable != null) {
|
||||||
|
dbCleanupHandler.postDelayed(dbCleanupRunnable, DB_CLEANUP_INTERVAL);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка при очистке БД от устаревших AIS целей: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Освобождает ресурсы
|
* Освобождает ресурсы
|
||||||
*/
|
*/
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
stopAllListeners();
|
stopAllListeners();
|
||||||
|
stopDatabaseCleanup();
|
||||||
|
|
||||||
if (udpListener != null) {
|
if (udpListener != null) {
|
||||||
udpListener.cleanup();
|
udpListener.cleanup();
|
||||||
@@ -595,11 +773,51 @@ public class AppController implements
|
|||||||
gpsLocationListener.cleanup();
|
gpsLocationListener.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (notificationService != null) {
|
||||||
|
notificationService.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
if (executor != null && !executor.isShutdown()) {
|
if (executor != null && !executor.isShutdown()) {
|
||||||
executor.shutdown();
|
executor.shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== Метки времени последних сообщений ($ и !) =====
|
||||||
|
private void updateLastMessageAgesFromRaw(String raw) {
|
||||||
|
if (raw == null) return;
|
||||||
|
long now = android.os.SystemClock.elapsedRealtime();
|
||||||
|
String[] lines = raw.split("\r?\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
if (line == null) continue;
|
||||||
|
String t = line.trim();
|
||||||
|
if (t.isEmpty()) continue;
|
||||||
|
char c = t.charAt(0);
|
||||||
|
if (c == '$') {
|
||||||
|
lastGPSMessageRealtimeMs = now;
|
||||||
|
break;
|
||||||
|
} else if (c == '!') {
|
||||||
|
lastAISMessageRealtimeMs = now;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Возвращает секунды с последнего GPS ($) сообщения; -1 если не было */
|
||||||
|
public int getSecondsSinceLastGPSMessage() {
|
||||||
|
if (lastGPSMessageRealtimeMs <= 0) return -1;
|
||||||
|
long diff = android.os.SystemClock.elapsedRealtime() - lastGPSMessageRealtimeMs;
|
||||||
|
if (diff < 0) return 0;
|
||||||
|
return (int)(diff / 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Возвращает секунды с последнего AIS (!) сообщения; -1 если не было */
|
||||||
|
public int getSecondsSinceLastAISMessage() {
|
||||||
|
if (lastAISMessageRealtimeMs <= 0) return -1;
|
||||||
|
long diff = android.os.SystemClock.elapsedRealtime() - lastAISMessageRealtimeMs;
|
||||||
|
if (diff < 0) return 0;
|
||||||
|
return (int)(diff / 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
// Методы для управления настройками
|
// Методы для управления настройками
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -952,8 +952,26 @@ public class NMEAParser {
|
|||||||
|
|
||||||
// Rate of Turn (8 бит) - бит 42
|
// Rate of Turn (8 бит) - бит 42
|
||||||
String rotBits = decodeAISField(payload, 42, 8);
|
String rotBits = decodeAISField(payload, 42, 8);
|
||||||
int rot = Integer.parseInt(rotBits, 2);
|
int rotRaw = Integer.parseInt(rotBits, 2);
|
||||||
Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rot);
|
if (rotRaw > 127) {
|
||||||
|
rotRaw -= 256;
|
||||||
|
}
|
||||||
|
double rateOfTurn = parseRateOfTurn(rotRaw);
|
||||||
|
Log.d(TAG, "Rate of Turn bits: " + rotBits + " = " + rotRaw + " -> " + rateOfTurn + " °/min");
|
||||||
|
|
||||||
|
// Дополнительная отладка - показываем все биты payload
|
||||||
|
String fullBinary = payloadToBinary(payload);
|
||||||
|
Log.d(TAG, "Full payload binary: " + fullBinary);
|
||||||
|
Log.d(TAG, "ROT bits 42-49: " + fullBinary.substring(42, Math.min(50, fullBinary.length())));
|
||||||
|
|
||||||
|
// Ищем ROT в разных позициях для отладки
|
||||||
|
// for (int pos = 0; pos < Math.min(fullBinary.length() - 8, 100); pos++) {
|
||||||
|
// String testBits = fullBinary.substring(pos, pos + 8);
|
||||||
|
// int testValue = Integer.parseInt(testBits, 2);
|
||||||
|
// double testRot = parseRateOfTurn(testValue);
|
||||||
|
// Log.d(TAG, String.format("Position %d: bits=%s, value=%d, rot=%.1f",
|
||||||
|
// pos, testBits, testValue, testRot));
|
||||||
|
// }
|
||||||
|
|
||||||
// Speed Over Ground (10 бит) - бит 50
|
// Speed Over Ground (10 бит) - бит 50
|
||||||
String speedBits = decodeAISField(payload, 50, 10);
|
String speedBits = decodeAISField(payload, 50, 10);
|
||||||
@@ -998,20 +1016,44 @@ public class NMEAParser {
|
|||||||
Log.w(TAG, "Долгота вне допустимых пределов: " + longitude);
|
Log.w(TAG, "Долгота вне допустимых пределов: " + longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, String.format("AIS Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%d, heading=%.1f",
|
Log.d(TAG, String.format("AIS Position: MMSI=%d, lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%d, heading=%.1f, rot=%.1f",
|
||||||
mmsi, latitude, longitude, course, speed, status, heading));
|
mmsi, latitude, longitude, course, speed, status, heading, rateOfTurn));
|
||||||
|
|
||||||
// Создаем или обновляем AIS судно
|
// Создаем или обновляем AIS судно
|
||||||
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
||||||
vessel.updatePosition(latitude, longitude, course, speed);
|
vessel.updatePosition(latitude, longitude, course, speed, rateOfTurn);
|
||||||
|
vessel.setPositionAccuracy(accuracy == 1);
|
||||||
vessel.setHeading(heading);
|
vessel.setHeading(heading);
|
||||||
vessel.setNavigationalStatus(getNavigationStatus(status));
|
vessel.setNavigationalStatus(getNavigationStatus(status));
|
||||||
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
|
// Помечаем класс судна как Class A, чтобы предотвратить дальнейшее перезаписывание Class B сообщениями
|
||||||
|
vessel.setVesselClass("Class A");
|
||||||
|
|
||||||
// Отправляем информацию о корабле на внешний ресурс
|
// Отправляем информацию о корабле на внешний ресурс (помечаем как Class A и добавляем статические поля, если известны)
|
||||||
String vesselInfo = String.format("lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, status=%s",
|
StringBuilder infoA = new StringBuilder(
|
||||||
latitude, longitude, course, speed, getNavigationStatus(status));
|
String.format(java.util.Locale.US,
|
||||||
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
"Class A: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, rot=%.1f, status=%s",
|
||||||
|
latitude, longitude, course, speed, rateOfTurn, getNavigationStatus(status))
|
||||||
|
);
|
||||||
|
if (vessel.getVesselName() != null && !vessel.getVesselName().trim().isEmpty()) {
|
||||||
|
infoA.append(String.format(java.util.Locale.US, ", name='%s'", vessel.getVesselName()));
|
||||||
|
}
|
||||||
|
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
|
||||||
|
infoA.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
|
||||||
|
}
|
||||||
|
if (vessel.getVesselType() != null && !vessel.getVesselType().trim().isEmpty()) {
|
||||||
|
infoA.append(String.format(java.util.Locale.US, ", type=%s", vessel.getVesselType()));
|
||||||
|
}
|
||||||
|
if (vessel.getLength() > 0 || vessel.getWidth() > 0) {
|
||||||
|
infoA.append(String.format(java.util.Locale.US, ", L=%.1f, W=%.1f", vessel.getLength(), vessel.getWidth()));
|
||||||
|
}
|
||||||
|
if (vessel.getDraft() > 0) {
|
||||||
|
infoA.append(String.format(java.util.Locale.US, ", D=%.1f", vessel.getDraft()));
|
||||||
|
}
|
||||||
|
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
|
||||||
|
infoA.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
|
||||||
|
}
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), infoA.toString());
|
||||||
|
|
||||||
// Уведомляем слушателя
|
// Уведомляем слушателя
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
@@ -1147,8 +1189,9 @@ public class NMEAParser {
|
|||||||
vessel.setEta(etaDateTime); // Добавляем ETA в модель
|
vessel.setEta(etaDateTime); // Добавляем ETA в модель
|
||||||
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
|
|
||||||
// Отправляем информацию о корабле на внешний ресурс
|
// Отправляем информацию о корабле на внешний ресурс (помечаем как Class A Static)
|
||||||
String vesselInfo = String.format("name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f, dest='%s'",
|
String vesselInfo = String.format(java.util.Locale.US,
|
||||||
|
"Class A Static: name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f, dest='%s'",
|
||||||
vesselName, callSign, getVesselType(vesselTypeCode), length, width, draft, destination);
|
vesselName, callSign, getVesselType(vesselTypeCode), length, width, draft, destination);
|
||||||
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
||||||
|
|
||||||
@@ -1207,6 +1250,48 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Преобразует AIS payload в полную битовую строку для отладки
|
||||||
|
*/
|
||||||
|
private String payloadToBinary(String payload) {
|
||||||
|
StringBuilder result = new StringBuilder();
|
||||||
|
for (int i = 0; i < payload.length(); i++) {
|
||||||
|
int ascii = payload.charAt(i);
|
||||||
|
int value;
|
||||||
|
if (ascii >= 48 && ascii <= 87) {
|
||||||
|
value = ascii - 48;
|
||||||
|
} else if (ascii >= 88 && ascii <= 119) {
|
||||||
|
value = ascii - 56;
|
||||||
|
} else {
|
||||||
|
value = 0;
|
||||||
|
}
|
||||||
|
String binary = String.format("%6s", Integer.toBinaryString(value)).replace(' ', '0');
|
||||||
|
result.append(binary);
|
||||||
|
}
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Парсит Rate of Turn согласно стандарту AIS
|
||||||
|
* ROTAIS = 4.733 SQRT(ROTINDICATED) degrees/min
|
||||||
|
* Значения: 0-126 = поворот вправо, 127 = поворот влево >5°/30с, 128-255 = поворот влево
|
||||||
|
*/
|
||||||
|
private double parseRateOfTurn(int rotRaw) {
|
||||||
|
if (rotRaw == -128) {
|
||||||
|
return Double.NaN; // Нет данных
|
||||||
|
}
|
||||||
|
if (rotRaw == -127) {
|
||||||
|
return -720.0; // Влево > 708°/мин
|
||||||
|
}
|
||||||
|
if (rotRaw == 127) {
|
||||||
|
return 720.0; // Вправо > 708°/мин
|
||||||
|
}
|
||||||
|
|
||||||
|
// В диапазоне -126..126
|
||||||
|
double rot = rotRaw / 4.733;
|
||||||
|
return Math.signum(rotRaw) * rot * rot;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Парсит AIS координаты
|
* Парсит AIS координаты
|
||||||
*/
|
*/
|
||||||
@@ -1235,31 +1320,95 @@ public class NMEAParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Декодирует AIS строку
|
* Декодирует AIS строку согласно стандарту ITU-R M.1371-5, таблица 44
|
||||||
|
* Простой switch case для всех 64 возможных значений 6-битной кодировки
|
||||||
*/
|
*/
|
||||||
//TODO: Исправить на нормальный декодер строк
|
|
||||||
private String decodeAISString(String bits) {
|
private String decodeAISString(String bits) {
|
||||||
StringBuilder result = new StringBuilder();
|
StringBuilder result = new StringBuilder();
|
||||||
|
|
||||||
|
Log.d(TAG, "Декодируем AIS строку, биты: " + bits + " (длина: " + bits.length() + ")");
|
||||||
|
|
||||||
for (int i = 0; i + 6 <= bits.length(); i += 6) {
|
for (int i = 0; i + 6 <= bits.length(); i += 6) {
|
||||||
String charBits = bits.substring(i, i + 6);
|
String charBits = bits.substring(i, i + 6);
|
||||||
int value = Integer.parseInt(charBits, 2);
|
int value = Integer.parseInt(charBits, 2);
|
||||||
|
|
||||||
char decodedChar;
|
char decodedChar;
|
||||||
if (value == 0) {
|
// Простой switch case для всех 64 возможных значений
|
||||||
decodedChar = ' '; // 0 = пробел
|
switch (value) {
|
||||||
} else if (value >= 1 && value <= 26) {
|
case 0: decodedChar = ' '; break;
|
||||||
decodedChar = (char) ('A' + value - 1); // 1..26 = A..Z
|
case 1: decodedChar = 'A'; break;
|
||||||
} else if (value >= 27 && value <= 36) {
|
case 2: decodedChar = 'B'; break;
|
||||||
decodedChar = (char) ('0' + (value - 27)); // 27..36 = 0..9
|
case 3: decodedChar = 'C'; break;
|
||||||
} else {
|
case 4: decodedChar = 'D'; break;
|
||||||
decodedChar = ' '; // всё остальное = пробел
|
case 5: decodedChar = 'E'; break;
|
||||||
|
case 6: decodedChar = 'F'; break;
|
||||||
|
case 7: decodedChar = 'G'; break;
|
||||||
|
case 8: decodedChar = 'H'; break;
|
||||||
|
case 9: decodedChar = 'I'; break;
|
||||||
|
case 10: decodedChar = 'J'; break;
|
||||||
|
case 11: decodedChar = 'K'; break;
|
||||||
|
case 12: decodedChar = 'L'; break;
|
||||||
|
case 13: decodedChar = 'M'; break;
|
||||||
|
case 14: decodedChar = 'N'; break;
|
||||||
|
case 15: decodedChar = 'O'; break;
|
||||||
|
case 16: decodedChar = 'P'; break;
|
||||||
|
case 17: decodedChar = 'Q'; break;
|
||||||
|
case 18: decodedChar = 'R'; break;
|
||||||
|
case 19: decodedChar = 'S'; break;
|
||||||
|
case 20: decodedChar = 'T'; break;
|
||||||
|
case 21: decodedChar = 'U'; break;
|
||||||
|
case 22: decodedChar = 'V'; break;
|
||||||
|
case 23: decodedChar = 'W'; break;
|
||||||
|
case 24: decodedChar = 'X'; break;
|
||||||
|
case 25: decodedChar = 'Y'; break;
|
||||||
|
case 26: decodedChar = 'Z'; break;
|
||||||
|
case 27: decodedChar = '0'; break;
|
||||||
|
case 28: decodedChar = '1'; break;
|
||||||
|
case 29: decodedChar = '2'; break;
|
||||||
|
case 30: decodedChar = '3'; break;
|
||||||
|
case 31: decodedChar = '4'; break;
|
||||||
|
case 32: decodedChar = ' '; break; // пробел
|
||||||
|
case 33: decodedChar = '5'; break;
|
||||||
|
case 34: decodedChar = '6'; break;
|
||||||
|
case 35: decodedChar = '7'; break;
|
||||||
|
case 36: decodedChar = '8'; break;
|
||||||
|
case 37: decodedChar = '9'; break;
|
||||||
|
case 38: decodedChar = ' '; break; // пробел
|
||||||
|
case 39: decodedChar = ' '; break; // пробел
|
||||||
|
case 40: decodedChar = ' '; break; // пробел
|
||||||
|
case 41: decodedChar = ' '; break; // пробел
|
||||||
|
case 42: decodedChar = ' '; break; // пробел
|
||||||
|
case 43: decodedChar = ' '; break; // пробел
|
||||||
|
case 44: decodedChar = ' '; break; // пробел
|
||||||
|
case 45: decodedChar = ' '; break; // пробел
|
||||||
|
case 46: decodedChar = ' '; break; // пробел
|
||||||
|
case 47: decodedChar = ' '; break; // пробел
|
||||||
|
case 48: decodedChar = '0'; break; // пробел
|
||||||
|
case 49: decodedChar = '1'; break; // пробел
|
||||||
|
case 50: decodedChar = '2'; break; // пробел
|
||||||
|
case 51: decodedChar = '3'; break; // пробел
|
||||||
|
case 52: decodedChar = '4'; break; // пробел
|
||||||
|
case 53: decodedChar = '5'; break; // пробел
|
||||||
|
case 54: decodedChar = '6'; break; // пробел
|
||||||
|
case 55: decodedChar = '7'; break; // пробел
|
||||||
|
case 56: decodedChar = '8'; break; // пробел
|
||||||
|
case 57: decodedChar = '9'; break; // пробел
|
||||||
|
case 58: decodedChar = ' '; break; // пробел
|
||||||
|
case 59: decodedChar = ' '; break; // пробел
|
||||||
|
case 60: decodedChar = ' '; break; // пробел
|
||||||
|
case 61: decodedChar = ' '; break; // пробел
|
||||||
|
case 62: decodedChar = ' '; break; // пробел
|
||||||
|
case 63: decodedChar = ' '; break; // пробел
|
||||||
|
default: decodedChar = ' '; break; // на всякий случай
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Символ " + (i/6 + 1) + ": биты=" + charBits + ", значение=" + value + ", символ='" + decodedChar + "'");
|
||||||
result.append(decodedChar);
|
result.append(decodedChar);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.toString().trim();
|
String resultStr = result.toString().trim();
|
||||||
|
Log.d(TAG, "Результат декодирования: '" + resultStr + "'");
|
||||||
|
return resultStr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1703,6 +1852,12 @@ public class NMEAParser {
|
|||||||
Log.d(TAG, "Safety Text bits: " + textBits + " = '" + safetyText + "'");
|
Log.d(TAG, "Safety Text bits: " + textBits + " = '" + safetyText + "'");
|
||||||
|
|
||||||
Log.d(TAG, String.format("AIS Safety Broadcast: MMSI=%d, text='%s'", mmsi, safetyText));
|
Log.d(TAG, String.format("AIS Safety Broadcast: MMSI=%d, text='%s'", mmsi, safetyText));
|
||||||
|
// Отправляем лог наружу
|
||||||
|
try {
|
||||||
|
com.grigowashere.aismap.utils.LogSender.logShipUpdate(String.valueOf(mmsi), "Safety: " + safetyText);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Log.w(TAG, "Ошибка отправки safety-лога: " + t.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// Создаем или обновляем AIS судно
|
// Создаем или обновляем AIS судно
|
||||||
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
||||||
@@ -1781,19 +1936,52 @@ public class NMEAParser {
|
|||||||
|
|
||||||
// Создаем или обновляем AIS судно
|
// Создаем или обновляем AIS судно
|
||||||
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
||||||
|
// Логика приоритета классов:
|
||||||
|
// - Если уже Class A: игнорируем обновление типа 18 полностью
|
||||||
|
// - Если Extended Class B: обновляем только динамику (позиция, скорость, курс и т.п.), класс не меняем
|
||||||
|
String existingClass = vessel.getVesselClass();
|
||||||
|
if ("Class A".equals(existingClass)) {
|
||||||
|
Log.d(TAG, "Пропускаем обновление Class B (тип 18) для судна класса Class A: " + mmsi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean keepExtended = "Extended Class B".equals(existingClass);
|
||||||
vessel.updatePosition(latitude, longitude, course, speed);
|
vessel.updatePosition(latitude, longitude, course, speed);
|
||||||
vessel.setHeading(heading);
|
vessel.setHeading(heading);
|
||||||
vessel.setPositionAccuracy(accuracy == 1);
|
vessel.setPositionAccuracy(accuracy == 1);
|
||||||
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
vessel.setLastUpdate(java.time.LocalDateTime.now());
|
||||||
|
if (!keepExtended) {
|
||||||
vessel.setVesselClass("Class B");
|
vessel.setVesselClass("Class B");
|
||||||
|
}
|
||||||
|
|
||||||
// В Class B Position Report размеры не передаются, но мы сохраняем существующие
|
// В Class B Position Report размеры не передаются, но мы сохраняем существующие
|
||||||
Log.d(TAG, "Class B Position Report - размеры не передаются, сохраняем существующие: L=" + vessel.getLength() + ", W=" + vessel.getWidth());
|
Log.d(TAG, "Class B Position Report - размеры не передаются, сохраняем существующие: L=" + vessel.getLength() + ", W=" + vessel.getWidth());
|
||||||
|
|
||||||
// Отправляем информацию о корабле на внешний ресурс
|
// Отправляем информацию о корабле на внешний ресурс
|
||||||
String vesselInfo = String.format("Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
|
// Добавляем статические поля, если они уже известны (из сообщений 24 и др.)
|
||||||
latitude, longitude, course, speed, heading, accuracy == 1 ? "high" : "low");
|
StringBuilder info = new StringBuilder(
|
||||||
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
String.format(java.util.Locale.US,
|
||||||
|
"Class B: lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
|
||||||
|
latitude, longitude, course, speed, heading, (accuracy == 1 ? "high" : "low"))
|
||||||
|
);
|
||||||
|
if (vessel.getVesselName() != null && !vessel.getVesselName().trim().isEmpty()) {
|
||||||
|
info.append(String.format(java.util.Locale.US, ", name='%s'", vessel.getVesselName()));
|
||||||
|
}
|
||||||
|
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
|
||||||
|
info.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
|
||||||
|
}
|
||||||
|
if (vessel.getVesselType() != null && !vessel.getVesselType().trim().isEmpty()) {
|
||||||
|
info.append(String.format(java.util.Locale.US, ", type=%s", vessel.getVesselType()));
|
||||||
|
}
|
||||||
|
if (vessel.getLength() > 0 || vessel.getWidth() > 0) {
|
||||||
|
info.append(String.format(java.util.Locale.US, ", L=%.1f, W=%.1f", vessel.getLength(), vessel.getWidth()));
|
||||||
|
}
|
||||||
|
if (vessel.getDraft() > 0) {
|
||||||
|
info.append(String.format(java.util.Locale.US, ", D=%.1f", vessel.getDraft()));
|
||||||
|
}
|
||||||
|
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
|
||||||
|
info.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
|
||||||
|
}
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), info.toString());
|
||||||
|
|
||||||
// Уведомляем слушателя
|
// Уведомляем слушателя
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
@@ -1895,6 +2083,12 @@ public class NMEAParser {
|
|||||||
Log.w(TAG, "Extended Class B - недостаточно битов для размеров: " + totalBits + " < 327");
|
Log.w(TAG, "Extended Class B - недостаточно битов для размеров: " + totalBits + " < 327");
|
||||||
// Создаем судно без размеров
|
// Создаем судно без размеров
|
||||||
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
||||||
|
// Если судно уже Class A, не перезаписываем данными Extended Class B
|
||||||
|
String existingClassShort = vessel.getVesselClass();
|
||||||
|
if ("Class A".equals(existingClassShort)) {
|
||||||
|
Log.d(TAG, "Пропускаем обновление Extended Class B для судна класса Class A: " + mmsi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
vessel.updatePosition(latitude, longitude, course, speed);
|
vessel.updatePosition(latitude, longitude, course, speed);
|
||||||
vessel.setHeading(heading);
|
vessel.setHeading(heading);
|
||||||
vessel.setPositionAccuracy(accuracy == 1);
|
vessel.setPositionAccuracy(accuracy == 1);
|
||||||
@@ -1906,6 +2100,19 @@ public class NMEAParser {
|
|||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
}
|
}
|
||||||
|
// Логируем короткое сообщение типа 19 с доступными данными
|
||||||
|
StringBuilder shortInfo = new StringBuilder(
|
||||||
|
String.format(java.util.Locale.US,
|
||||||
|
"Extended Class B (short): name='%s', lat=%.6f, lon=%.6f, course=%.1f, speed=%.1f, heading=%.1f, accuracy=%s",
|
||||||
|
vesselName, latitude, longitude, course, speed, heading, (accuracy == 1 ? "high" : "low"))
|
||||||
|
);
|
||||||
|
if (vessel.getCallSign() != null && !vessel.getCallSign().trim().isEmpty()) {
|
||||||
|
shortInfo.append(String.format(java.util.Locale.US, ", callSign='%s'", vessel.getCallSign()));
|
||||||
|
}
|
||||||
|
if (vessel.getDestination() != null && !vessel.getDestination().trim().isEmpty()) {
|
||||||
|
shortInfo.append(String.format(java.util.Locale.US, ", dest='%s'", vessel.getDestination()));
|
||||||
|
}
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), shortInfo.toString());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1970,6 +2177,12 @@ public class NMEAParser {
|
|||||||
|
|
||||||
// Создаем или обновляем AIS судно
|
// Создаем или обновляем AIS судно
|
||||||
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
AISVessel vessel = findOrCreateAISVessel(String.valueOf(mmsi));
|
||||||
|
// Если судно уже Class A, не перезаписываем данными Extended Class B
|
||||||
|
String existingClassFull = vessel.getVesselClass();
|
||||||
|
if ("Class A".equals(existingClassFull)) {
|
||||||
|
Log.d(TAG, "Пропускаем обновление Extended Class B для судна класса Class A: " + mmsi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
vessel.updatePosition(latitude, longitude, course, speed);
|
vessel.updatePosition(latitude, longitude, course, speed);
|
||||||
vessel.setHeading(heading);
|
vessel.setHeading(heading);
|
||||||
vessel.setPositionAccuracy(accuracy == 1);
|
vessel.setPositionAccuracy(accuracy == 1);
|
||||||
@@ -2126,6 +2339,12 @@ public class NMEAParser {
|
|||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Логируем статические данные Class B (Part A)
|
||||||
|
String vesselInfo = String.format(java.util.Locale.US,
|
||||||
|
"Class B Static A: name='%s'",
|
||||||
|
vesselName);
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfo);
|
||||||
|
|
||||||
} else if (partNumber == 1) {
|
} else if (partNumber == 1) {
|
||||||
// Part B: Vessel Type, Dimensions, etc.
|
// Part B: Vessel Type, Dimensions, etc.
|
||||||
String typeBits = decodeAISField(payload, 40, 8);
|
String typeBits = decodeAISField(payload, 40, 8);
|
||||||
@@ -2212,6 +2431,15 @@ public class NMEAParser {
|
|||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onAISVesselUpdated(vessel);
|
listener.onAISVesselUpdated(vessel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Логируем статические данные Class B (Part B)
|
||||||
|
String vesselInfoB = String.format(java.util.Locale.US,
|
||||||
|
"Class B Static B: name='%s', callSign='%s', type=%s, L=%.1f, W=%.1f, D=%.1f",
|
||||||
|
vessel.getVesselName() != null ? vessel.getVesselName() : "",
|
||||||
|
callSign,
|
||||||
|
getVesselType(vesselTypeCode),
|
||||||
|
length, width, draft);
|
||||||
|
LogSender.logShipUpdate(String.valueOf(mmsi), vesselInfoB);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package com.grigowashere.aismap.data;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.room.Database;
|
||||||
|
import androidx.room.Room;
|
||||||
|
import androidx.room.RoomDatabase;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.dao.AISVesselDao;
|
||||||
|
import com.grigowashere.aismap.data.dao.VesselDao;
|
||||||
|
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||||
|
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||||
|
|
||||||
|
@Database(entities = {AISVesselEntity.class, VesselEntity.class}, version = 3, exportSchema = false)
|
||||||
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
public abstract AISVesselDao aisVesselDao();
|
||||||
|
public abstract VesselDao vesselDao();
|
||||||
|
|
||||||
|
private static volatile AppDatabase INSTANCE;
|
||||||
|
|
||||||
|
public static AppDatabase getInstance(Context context) {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
synchronized (AppDatabase.class) {
|
||||||
|
if (INSTANCE == null) {
|
||||||
|
INSTANCE = Room.databaseBuilder(
|
||||||
|
context.getApplicationContext(),
|
||||||
|
AppDatabase.class,
|
||||||
|
"aismap.db"
|
||||||
|
).fallbackToDestructiveMigration().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package com.grigowashere.aismap.data;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.dao.AISVesselDao;
|
||||||
|
import com.grigowashere.aismap.data.dao.VesselDao;
|
||||||
|
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||||
|
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
public class Repository {
|
||||||
|
private final AISVesselDao aisVesselDao;
|
||||||
|
private final VesselDao vesselDao;
|
||||||
|
private final ExecutorService ioExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
public Repository(Context context) {
|
||||||
|
AppDatabase db = AppDatabase.getInstance(context);
|
||||||
|
this.aisVesselDao = db.aisVesselDao();
|
||||||
|
this.vesselDao = db.vesselDao();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsertAIS(AISVesselEntity entity) {
|
||||||
|
ioExecutor.execute(() -> aisVesselDao.upsert(entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteStaleAIS(long thresholdEpochMs) {
|
||||||
|
ioExecutor.execute(() -> aisVesselDao.deleteStale(thresholdEpochMs));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AISVesselEntity> getAllAISSync() {
|
||||||
|
return aisVesselDao.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<List<AISVesselEntity>> observeAllAIS() {
|
||||||
|
return aisVesselDao.observeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public AISVesselEntity getAISByMmsiSync(String mmsi) {
|
||||||
|
return aisVesselDao.getByMmsi(mmsi);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void upsertOwnVessel(VesselEntity entity) {
|
||||||
|
ioExecutor.execute(() -> {
|
||||||
|
vesselDao.upsert(entity);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public VesselEntity getLatestOwnVesselSync() {
|
||||||
|
return vesselDao.getLatest();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.grigowashere.aismap.data.dao;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Update;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface AISVesselDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
void upsert(AISVesselEntity entity);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
void update(AISVesselEntity entity);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ais_vessels")
|
||||||
|
List<AISVesselEntity> getAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ais_vessels")
|
||||||
|
LiveData<List<AISVesselEntity>> observeAll();
|
||||||
|
|
||||||
|
@Query("SELECT * FROM ais_vessels WHERE mmsi = :mmsi LIMIT 1")
|
||||||
|
AISVesselEntity getByMmsi(String mmsi);
|
||||||
|
|
||||||
|
@Query("DELETE FROM ais_vessels WHERE lastUpdateEpochMs < :threshold")
|
||||||
|
void deleteStale(long threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.grigowashere.aismap.data.dao;
|
||||||
|
|
||||||
|
import androidx.room.Dao;
|
||||||
|
import androidx.room.Insert;
|
||||||
|
import androidx.room.OnConflictStrategy;
|
||||||
|
import androidx.room.Query;
|
||||||
|
import androidx.room.Update;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.entity.VesselEntity;
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
public interface VesselDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
long upsert(VesselEntity entity);
|
||||||
|
|
||||||
|
@Update
|
||||||
|
void update(VesselEntity entity);
|
||||||
|
|
||||||
|
@Query("SELECT * FROM own_vessel ORDER BY id DESC LIMIT 1")
|
||||||
|
VesselEntity getLatest();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.grigowashere.aismap.data.entity;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room-сущность для хранения AIS цели
|
||||||
|
* Теперь содержит ВСЕ поля из AISVessel модели
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "ais_vessels")
|
||||||
|
public class AISVesselEntity {
|
||||||
|
@PrimaryKey
|
||||||
|
@NonNull
|
||||||
|
public String mmsi;
|
||||||
|
|
||||||
|
// Основная информация о судне
|
||||||
|
public String vesselName;
|
||||||
|
public String callSign;
|
||||||
|
public int imo; // IMO номер
|
||||||
|
public String vesselType; // тип судна
|
||||||
|
|
||||||
|
// Позиция и движение
|
||||||
|
public double latitude;
|
||||||
|
public double longitude;
|
||||||
|
public double course; // курс в градусах (0-360)
|
||||||
|
public double speed; // скорость в узлах
|
||||||
|
public double heading; // направление движения в градусах
|
||||||
|
public double rateOfTurn; // скорость поворота в градусах/минуту
|
||||||
|
|
||||||
|
// Размеры судна
|
||||||
|
public double length; // длина судна в метрах
|
||||||
|
public double width; // ширина судна в метрах
|
||||||
|
public double draft; // осадка в метрах
|
||||||
|
|
||||||
|
// Навигационная информация
|
||||||
|
public String destination; // пункт назначения
|
||||||
|
public long etaEpochMs; // предполагаемое время прибытия (epoch ms)
|
||||||
|
public String navigationalStatus; // навигационный статус
|
||||||
|
public boolean positionAccuracy; // точность позиции
|
||||||
|
|
||||||
|
// Техническая информация
|
||||||
|
public int signalStrength; // сила AIS сигнала
|
||||||
|
public String vesselClass; // класс судна (Class A, Class B, Extended Class B)
|
||||||
|
public String vendorId; // идентификатор производителя оборудования
|
||||||
|
public String lastSafetyMessage; // последнее сообщение безопасности
|
||||||
|
|
||||||
|
// Состояние и время
|
||||||
|
public long lastUpdateEpochMs; // время последнего обновления (epoch ms)
|
||||||
|
public boolean isActive; // активно ли судно
|
||||||
|
public boolean selected; // выделено ли судно на карте
|
||||||
|
|
||||||
|
public AISVesselEntity(@NonNull String mmsi) {
|
||||||
|
this.mmsi = mmsi;
|
||||||
|
this.isActive = true;
|
||||||
|
this.selected = false;
|
||||||
|
this.lastUpdateEpochMs = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.grigowashere.aismap.data.entity;
|
||||||
|
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Room-сущность для хранения нашего судна/позиции
|
||||||
|
*/
|
||||||
|
@Entity(tableName = "own_vessel")
|
||||||
|
public class VesselEntity {
|
||||||
|
@PrimaryKey(autoGenerate = true)
|
||||||
|
public long id;
|
||||||
|
|
||||||
|
public double latitude;
|
||||||
|
public double longitude;
|
||||||
|
public double course;
|
||||||
|
public double speed;
|
||||||
|
public double heading;
|
||||||
|
public float accuracy;
|
||||||
|
public long fixTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.grigowashere.aismap.data.mapper;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.data.entity.AISVesselEntity;
|
||||||
|
import com.grigowashere.aismap.models.AISVessel;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Маппер для конвертации между AISVessel (модель) и AISVesselEntity (БД)
|
||||||
|
* Решает проблему потери данных при сохранении/восстановлении AIS судов
|
||||||
|
*/
|
||||||
|
public class AISVesselMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует AISVessel модель в AISVesselEntity для сохранения в БД
|
||||||
|
*/
|
||||||
|
public static AISVesselEntity toEntity(AISVessel vessel) {
|
||||||
|
if (vessel == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AISVesselEntity entity = new AISVesselEntity(vessel.getMmsi());
|
||||||
|
|
||||||
|
// Основная информация о судне
|
||||||
|
entity.vesselName = vessel.getVesselName();
|
||||||
|
entity.callSign = vessel.getCallSign();
|
||||||
|
entity.imo = vessel.getImo();
|
||||||
|
entity.vesselType = vessel.getVesselType();
|
||||||
|
|
||||||
|
// Позиция и движение
|
||||||
|
entity.latitude = vessel.getLatitude();
|
||||||
|
entity.longitude = vessel.getLongitude();
|
||||||
|
entity.course = vessel.getCourse();
|
||||||
|
entity.speed = vessel.getSpeed();
|
||||||
|
entity.heading = vessel.getHeading();
|
||||||
|
|
||||||
|
// Размеры судна
|
||||||
|
entity.length = vessel.getLength();
|
||||||
|
entity.width = vessel.getWidth();
|
||||||
|
entity.draft = vessel.getDraft();
|
||||||
|
|
||||||
|
// Навигационная информация
|
||||||
|
entity.destination = vessel.getDestination();
|
||||||
|
entity.etaEpochMs = convertLocalDateTimeToEpochMs(vessel.getEta());
|
||||||
|
entity.navigationalStatus = vessel.getNavigationalStatus();
|
||||||
|
entity.positionAccuracy = vessel.isPositionAccuracy();
|
||||||
|
|
||||||
|
// Техническая информация
|
||||||
|
entity.signalStrength = vessel.getSignalStrength();
|
||||||
|
entity.vesselClass = vessel.getVesselClass();
|
||||||
|
entity.vendorId = vessel.getVendorId();
|
||||||
|
entity.lastSafetyMessage = vessel.getLastSafetyMessage();
|
||||||
|
|
||||||
|
// Состояние и время
|
||||||
|
entity.lastUpdateEpochMs = convertLocalDateTimeToEpochMs(vessel.getLastUpdate());
|
||||||
|
entity.isActive = vessel.isActive();
|
||||||
|
entity.selected = vessel.isSelected();
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует AISVesselEntity из БД в AISVessel модель
|
||||||
|
*/
|
||||||
|
public static AISVessel toModel(AISVesselEntity entity) {
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AISVessel vessel = new AISVessel(entity.mmsi);
|
||||||
|
|
||||||
|
// Основная информация о судне
|
||||||
|
vessel.setVesselName(entity.vesselName);
|
||||||
|
vessel.setCallSign(entity.callSign);
|
||||||
|
vessel.setImo(entity.imo);
|
||||||
|
vessel.setVesselType(entity.vesselType);
|
||||||
|
|
||||||
|
// Позиция и движение
|
||||||
|
vessel.setLatitude(entity.latitude);
|
||||||
|
vessel.setLongitude(entity.longitude);
|
||||||
|
vessel.setCourse(entity.course);
|
||||||
|
vessel.setSpeed(entity.speed);
|
||||||
|
vessel.setHeading(entity.heading);
|
||||||
|
|
||||||
|
// Размеры судна
|
||||||
|
vessel.setLength(entity.length);
|
||||||
|
vessel.setWidth(entity.width);
|
||||||
|
vessel.setDraft(entity.draft);
|
||||||
|
|
||||||
|
// Навигационная информация
|
||||||
|
vessel.setDestination(entity.destination);
|
||||||
|
vessel.setEta(convertEpochMsToLocalDateTime(entity.etaEpochMs));
|
||||||
|
vessel.setNavigationalStatus(entity.navigationalStatus);
|
||||||
|
vessel.setPositionAccuracy(entity.positionAccuracy);
|
||||||
|
|
||||||
|
// Техническая информация
|
||||||
|
vessel.setSignalStrength(entity.signalStrength);
|
||||||
|
vessel.setVesselClass(entity.vesselClass);
|
||||||
|
vessel.setVendorId(entity.vendorId);
|
||||||
|
vessel.setLastSafetyMessage(entity.lastSafetyMessage);
|
||||||
|
|
||||||
|
// Состояние и время
|
||||||
|
vessel.setLastUpdate(convertEpochMsToLocalDateTime(entity.lastUpdateEpochMs));
|
||||||
|
vessel.setActive(entity.isActive);
|
||||||
|
vessel.setSelected(entity.selected);
|
||||||
|
|
||||||
|
return vessel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует LocalDateTime в epoch milliseconds
|
||||||
|
*/
|
||||||
|
private static long convertLocalDateTimeToEpochMs(LocalDateTime dateTime) {
|
||||||
|
if (dateTime == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return dateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Конвертирует epoch milliseconds в LocalDateTime
|
||||||
|
*/
|
||||||
|
private static LocalDateTime convertEpochMsToLocalDateTime(long epochMs) {
|
||||||
|
if (epochMs <= 0) {
|
||||||
|
return LocalDateTime.now();
|
||||||
|
}
|
||||||
|
return LocalDateTime.ofInstant(Instant.ofEpochMilli(epochMs), ZoneId.systemDefault());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,7 +76,9 @@ public class MapForgeImpl implements MapInterface {
|
|||||||
@Override
|
@Override
|
||||||
public void addAISVesselMarker(AISVessel vessel) {
|
public void addAISVesselMarker(AISVessel vessel) {
|
||||||
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
LatLong position = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
|
// Используем heading вместо course для поворота маркера AIS судна
|
||||||
|
double rotationAngle = vessel.getHeading() > 0 ? vessel.getHeading() : vessel.getCourse();
|
||||||
|
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, rotationAngle);
|
||||||
|
|
||||||
Marker marker = new Marker(position, icon, 0, 0);
|
Marker marker = new Marker(position, icon, 0, 0);
|
||||||
// MapForge не поддерживает OnTapListener напрямую
|
// MapForge не поддерживает OnTapListener напрямую
|
||||||
@@ -92,7 +94,9 @@ public class MapForgeImpl implements MapInterface {
|
|||||||
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
LatLong newPosition = new LatLong(vessel.getLatitude(), vessel.getLongitude());
|
||||||
marker.setLatLong(newPosition);
|
marker.setLatLong(newPosition);
|
||||||
|
|
||||||
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, vessel.getCourse());
|
// Используем heading вместо course для поворота маркера AIS судна
|
||||||
|
double rotationAngle = vessel.getHeading() > 0 ? vessel.getHeading() : vessel.getCourse();
|
||||||
|
org.mapsforge.core.graphics.Bitmap icon = createMapForgeIcon(Color.RED, rotationAngle);
|
||||||
marker.setBitmap(icon);
|
marker.setBitmap(icon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,4 +58,24 @@ public interface MarkerManager {
|
|||||||
* Получение количества активных маркеров
|
* Получение количества активных маркеров
|
||||||
*/
|
*/
|
||||||
int getActiveMarkerCount();
|
int getActiveMarkerCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает отображение путей движения
|
||||||
|
*/
|
||||||
|
void setPathTrackingEnabled(boolean enabled);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает путь конкретного судна
|
||||||
|
*/
|
||||||
|
void clearVesselPath(String mmsi);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает все пути движения
|
||||||
|
*/
|
||||||
|
void clearAllPaths();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет настройки отображения путей
|
||||||
|
*/
|
||||||
|
void updatePathSettings(int pathColor, int predictionColor, float pathWidth, float predictionWidth);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,329 @@
|
|||||||
|
package com.grigowashere.aismap.maps;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import com.yandex.mapkit.geometry.Point;
|
||||||
|
import com.yandex.mapkit.map.MapObjectCollection;
|
||||||
|
import com.yandex.mapkit.map.PolylineMapObject;
|
||||||
|
import com.yandex.mapkit.map.MapObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для отслеживания и отображения пути движения судна
|
||||||
|
* Отображает сплошную линию пройденного пути и прогнозируемое движение
|
||||||
|
*/
|
||||||
|
public class VesselPathTracker {
|
||||||
|
|
||||||
|
private static final int MAX_PATH_POINTS = 100; // Максимальное количество точек в пути
|
||||||
|
private static final long MIN_TIME_BETWEEN_POINTS = 1000; // Минимальное время между точками (1 секунда)
|
||||||
|
private static final double MIN_DISTANCE_BETWEEN_POINTS = 10.0; // Минимальное расстояние между точками (10 метров)
|
||||||
|
|
||||||
|
private String vesselId;
|
||||||
|
private MapObjectCollection mapObjects;
|
||||||
|
private ConcurrentLinkedQueue<PathPoint> pathHistory;
|
||||||
|
private PolylineMapObject pathLine;
|
||||||
|
private PolylineMapObject predictionLine;
|
||||||
|
private long lastUpdateTime;
|
||||||
|
private Point lastPosition;
|
||||||
|
|
||||||
|
|
||||||
|
// Настройки отображения
|
||||||
|
private int pathColor = Color.CYAN;
|
||||||
|
private int predictionColor = Color.YELLOW;
|
||||||
|
private float pathWidth = 3.0f;
|
||||||
|
private float predictionWidth = 2.0f;
|
||||||
|
private boolean isEnabled = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Точка пути с временной меткой
|
||||||
|
*/
|
||||||
|
private static class PathPoint {
|
||||||
|
public final Point position;
|
||||||
|
public final long timestamp;
|
||||||
|
public final double speed;
|
||||||
|
public final double course;
|
||||||
|
|
||||||
|
public PathPoint(Point position, long timestamp, double speed, double course) {
|
||||||
|
this.position = position;
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
this.speed = speed;
|
||||||
|
this.course = course;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public VesselPathTracker(String vesselId, MapObjectCollection mapObjects) {
|
||||||
|
this.vesselId = vesselId;
|
||||||
|
this.mapObjects = mapObjects;
|
||||||
|
this.pathHistory = new ConcurrentLinkedQueue<>();
|
||||||
|
this.lastUpdateTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет путь судна новой позицией
|
||||||
|
*/
|
||||||
|
public void updatePosition(double latitude, double longitude, double speed, double course) {
|
||||||
|
if (!isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
Point newPosition = new Point(latitude, longitude);
|
||||||
|
|
||||||
|
// Проверяем, нужно ли добавить новую точку
|
||||||
|
if (shouldAddPoint(newPosition, currentTime)) {
|
||||||
|
PathPoint newPoint = new PathPoint(newPosition, currentTime, speed, course);
|
||||||
|
pathHistory.offer(newPoint);
|
||||||
|
|
||||||
|
// Ограничиваем количество точек
|
||||||
|
while (pathHistory.size() > MAX_PATH_POINTS) {
|
||||||
|
pathHistory.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPosition = newPosition;
|
||||||
|
lastUpdateTime = currentTime;
|
||||||
|
|
||||||
|
// Обновляем отображение пути
|
||||||
|
updatePathDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, нужно ли добавить новую точку в путь
|
||||||
|
*/
|
||||||
|
private boolean shouldAddPoint(Point newPosition, long currentTime) {
|
||||||
|
// Проверяем время
|
||||||
|
if (currentTime - lastUpdateTime < MIN_TIME_BETWEEN_POINTS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем расстояние
|
||||||
|
if (lastPosition != null) {
|
||||||
|
double distance = calculateDistance(lastPosition, newPosition);
|
||||||
|
if (distance < MIN_DISTANCE_BETWEEN_POINTS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет отображение пути на карте
|
||||||
|
*/
|
||||||
|
private void updatePathDisplay() {
|
||||||
|
if (pathHistory.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mapObjects == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем список точек для пройденного пути
|
||||||
|
List<Point> pathPoints = new ArrayList<>();
|
||||||
|
for (PathPoint point : pathHistory) {
|
||||||
|
pathPoints.add(point.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем старые линии
|
||||||
|
try {
|
||||||
|
if (pathLine != null) {
|
||||||
|
mapObjects.remove(pathLine);
|
||||||
|
pathLine = null;
|
||||||
|
}
|
||||||
|
if (predictionLine != null) {
|
||||||
|
mapObjects.remove(predictionLine);
|
||||||
|
predictionLine = null;
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Коллекция могла быть инвалидирована (weak_ptr expired). Прекращаем обновления.
|
||||||
|
isEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем линию пройденного пути
|
||||||
|
if (pathPoints.size() > 1) {
|
||||||
|
try {
|
||||||
|
pathLine = mapObjects.addPolyline(new com.yandex.mapkit.geometry.Polyline(pathPoints));
|
||||||
|
if (pathLine != null) {
|
||||||
|
pathLine.setStrokeColor(pathColor);
|
||||||
|
pathLine.setStrokeWidth(pathWidth);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
isEnabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем линию прогнозируемого движения
|
||||||
|
createPredictionLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает линию прогнозируемого движения
|
||||||
|
*/
|
||||||
|
private void createPredictionLine() {
|
||||||
|
if (pathHistory.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mapObjects == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем последнюю точку
|
||||||
|
PathPoint lastPoint = null;
|
||||||
|
for (PathPoint point : pathHistory) {
|
||||||
|
lastPoint = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPoint == null || lastPoint.speed <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рассчитываем прогнозируемую позицию через 1 минуту
|
||||||
|
double predictionTimeMinutes = 1.0; // 1 минута
|
||||||
|
double predictionDistance = lastPoint.speed * predictionTimeMinutes * 60.0; // расстояние в метрах
|
||||||
|
|
||||||
|
// Конвертируем курс в радианы
|
||||||
|
double courseRad = Math.toRadians(lastPoint.course);
|
||||||
|
|
||||||
|
// Рассчитываем новую позицию
|
||||||
|
double earthRadius = 6371000; // радиус Земли в метрах
|
||||||
|
double lat1 = Math.toRadians(lastPoint.position.getLatitude());
|
||||||
|
double lon1 = Math.toRadians(lastPoint.position.getLongitude());
|
||||||
|
|
||||||
|
double lat2 = Math.asin(Math.sin(lat1) * Math.cos(predictionDistance / earthRadius) +
|
||||||
|
Math.cos(lat1) * Math.sin(predictionDistance / earthRadius) * Math.cos(courseRad));
|
||||||
|
|
||||||
|
double lon2 = lon1 + Math.atan2(Math.sin(courseRad) * Math.sin(predictionDistance / earthRadius) * Math.cos(lat1),
|
||||||
|
Math.cos(predictionDistance / earthRadius) - Math.sin(lat1) * Math.sin(lat2));
|
||||||
|
|
||||||
|
Point predictionPoint = new Point(Math.toDegrees(lat2), Math.toDegrees(lon2));
|
||||||
|
|
||||||
|
// Создаем линию прогноза
|
||||||
|
List<Point> predictionPoints = new ArrayList<>();
|
||||||
|
predictionPoints.add(lastPoint.position);
|
||||||
|
predictionPoints.add(predictionPoint);
|
||||||
|
|
||||||
|
try {
|
||||||
|
predictionLine = mapObjects.addPolyline(new com.yandex.mapkit.geometry.Polyline(predictionPoints));
|
||||||
|
if (predictionLine != null) {
|
||||||
|
predictionLine.setStrokeColor(predictionColor);
|
||||||
|
predictionLine.setStrokeWidth(predictionWidth);
|
||||||
|
// Сплошная линия для прогноза (по умолчанию)
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
isEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рассчитывает расстояние между двумя точками в метрах
|
||||||
|
*/
|
||||||
|
private double calculateDistance(Point point1, Point point2) {
|
||||||
|
double lat1 = Math.toRadians(point1.getLatitude());
|
||||||
|
double lon1 = Math.toRadians(point1.getLongitude());
|
||||||
|
double lat2 = Math.toRadians(point2.getLatitude());
|
||||||
|
double lon2 = Math.toRadians(point2.getLongitude());
|
||||||
|
|
||||||
|
double dlat = lat2 - lat1;
|
||||||
|
double dlon = lon2 - lon1;
|
||||||
|
|
||||||
|
double a = Math.sin(dlat / 2) * Math.sin(dlat / 2) +
|
||||||
|
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dlon / 2) * Math.sin(dlon / 2);
|
||||||
|
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
|
||||||
|
return 6371000 * c; // радиус Земли в метрах
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает путь судна
|
||||||
|
*/
|
||||||
|
public void clearPath() {
|
||||||
|
try {
|
||||||
|
if (pathLine != null && mapObjects != null) {
|
||||||
|
mapObjects.remove(pathLine);
|
||||||
|
pathLine = null;
|
||||||
|
}
|
||||||
|
if (predictionLine != null && mapObjects != null) {
|
||||||
|
mapObjects.remove(predictionLine);
|
||||||
|
predictionLine = null;
|
||||||
|
}
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Игнорируем ошибки очистки при невалидной коллекции
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pathHistory.clear();
|
||||||
|
lastPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет трекер пути
|
||||||
|
*/
|
||||||
|
public void remove() {
|
||||||
|
clearPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает отображение пути
|
||||||
|
*/
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
this.isEnabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
clearPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает цвет пройденного пути
|
||||||
|
*/
|
||||||
|
public void setPathColor(int color) {
|
||||||
|
this.pathColor = color;
|
||||||
|
if (pathLine != null) {
|
||||||
|
pathLine.setStrokeColor(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает цвет прогнозируемого пути
|
||||||
|
*/
|
||||||
|
public void setPredictionColor(int color) {
|
||||||
|
this.predictionColor = color;
|
||||||
|
if (predictionLine != null) {
|
||||||
|
predictionLine.setStrokeColor(color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает ширину линий
|
||||||
|
*/
|
||||||
|
public void setLineWidth(float pathWidth, float predictionWidth) {
|
||||||
|
this.pathWidth = pathWidth;
|
||||||
|
this.predictionWidth = predictionWidth;
|
||||||
|
|
||||||
|
if (pathLine != null) {
|
||||||
|
pathLine.setStrokeWidth(pathWidth);
|
||||||
|
}
|
||||||
|
if (predictionLine != null) {
|
||||||
|
predictionLine.setStrokeWidth(predictionWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, активен ли трекер
|
||||||
|
*/
|
||||||
|
public boolean isActive() {
|
||||||
|
return isEnabled && !pathHistory.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает количество точек в пути
|
||||||
|
*/
|
||||||
|
public int getPathPointCount() {
|
||||||
|
return pathHistory.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,7 +46,9 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
try {
|
try {
|
||||||
this.mapObjects = mapView.getMap().getMapObjects().addCollection();
|
this.mapObjects = mapView.getMap().getMapObjects().addCollection();
|
||||||
// Инициализируем менеджер маркеров
|
// Инициализируем менеджер маркеров
|
||||||
this.markerManager = new YandexMarkerManager(context, mapObjects, mapView);
|
com.grigowashere.aismap.utils.SettingsManager settingsManager =
|
||||||
|
new com.grigowashere.aismap.utils.SettingsManager(context);
|
||||||
|
this.markerManager = new YandexMarkerManager(context, mapObjects, mapView, settingsManager);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Ошибка создания коллекции объектов карты
|
// Ошибка создания коллекции объектов карты
|
||||||
}
|
}
|
||||||
@@ -129,7 +131,7 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
@Override
|
@Override
|
||||||
public void centerOnPosition(double latitude, double longitude) {
|
public void centerOnPosition(double latitude, double longitude) {
|
||||||
Point point = new Point(latitude, longitude);
|
Point point = new Point(latitude, longitude);
|
||||||
CameraPosition cameraPosition = new CameraPosition(point, 15.0f, 0.0f, 0.0f);
|
CameraPosition cameraPosition = new CameraPosition(point, 13.0f, 0.0f, 0.0f);
|
||||||
mapView.getMap().move(cameraPosition, new Animation(Animation.Type.SMOOTH, 1.0f), null);
|
mapView.getMap().move(cameraPosition, new Animation(Animation.Type.SMOOTH, 1.0f), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,6 +209,15 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительно обновляет все маркеры при изменении зума
|
||||||
|
*/
|
||||||
|
public void forceRefreshMarkersOnZoomChange() {
|
||||||
|
if (markerManager != null) {
|
||||||
|
markerManager.forceRefreshAllMarkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет и восстанавливает финализированные маркеры
|
* Проверяет и восстанавливает финализированные маркеры
|
||||||
*/
|
*/
|
||||||
@@ -226,6 +237,42 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает отображение путей движения
|
||||||
|
*/
|
||||||
|
public void setPathTrackingEnabled(boolean enabled) {
|
||||||
|
if (markerManager != null) {
|
||||||
|
markerManager.setPathTrackingEnabled(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает путь конкретного судна
|
||||||
|
*/
|
||||||
|
public void clearVesselPath(String mmsi) {
|
||||||
|
if (markerManager != null) {
|
||||||
|
markerManager.clearVesselPath(mmsi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очищает все пути движения
|
||||||
|
*/
|
||||||
|
public void clearAllPaths() {
|
||||||
|
if (markerManager != null) {
|
||||||
|
markerManager.clearAllPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет настройки отображения путей
|
||||||
|
*/
|
||||||
|
public void updatePathSettings(int pathColor, int predictionColor, float pathWidth, float predictionWidth) {
|
||||||
|
if (markerManager != null) {
|
||||||
|
markerManager.updatePathSettings(pathColor, predictionColor, pathWidth, predictionWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получение MapView для использования в layout
|
* Получение MapView для использования в layout
|
||||||
@@ -257,10 +304,11 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
// Включаем жесты поворота карты
|
// Включаем жесты поворота карты
|
||||||
mapView.getMap().setRotateGesturesEnabled(true);
|
mapView.getMap().setRotateGesturesEnabled(true);
|
||||||
|
|
||||||
// Добавляем слушатель изменений камеры для обновления маркеров при повороте
|
// Добавляем слушатель изменений камеры для обновления маркеров при повороте и зуме
|
||||||
mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() {
|
mapView.getMap().addCameraListener(new com.yandex.mapkit.map.CameraListener() {
|
||||||
private long lastUpdateTime = 0;
|
private long lastUpdateTime = 0;
|
||||||
private static final long UPDATE_THROTTLE = 50; // 50мс между обновлениями
|
private static final long UPDATE_THROTTLE = 200; // 200мс между обновлениями (увеличено для снижения нагрузки)
|
||||||
|
private float lastZoom = -1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCameraPositionChanged(com.yandex.mapkit.map.Map map,
|
public void onCameraPositionChanged(com.yandex.mapkit.map.Map map,
|
||||||
@@ -270,9 +318,23 @@ public class YandexMapImpl implements MapInterface {
|
|||||||
|
|
||||||
// Обновляем маркеры в реальном времени с throttling
|
// Обновляем маркеры в реальном времени с throttling
|
||||||
long currentTime = System.currentTimeMillis();
|
long currentTime = System.currentTimeMillis();
|
||||||
if (currentTime - lastUpdateTime >= UPDATE_THROTTLE) {
|
float currentZoom = cameraPosition.getZoom();
|
||||||
onMapRotationChanged();
|
|
||||||
|
// Проверяем, изменился ли зум значительно (больше чем на 1.0)
|
||||||
|
boolean zoomChanged = Math.abs(currentZoom - lastZoom) > 1.0f;
|
||||||
|
|
||||||
|
if (currentTime - lastUpdateTime >= UPDATE_THROTTLE || zoomChanged) {
|
||||||
|
//onMapRotationChanged();
|
||||||
|
// Обновляем маркеры только при значительных изменениях
|
||||||
|
if (zoomChanged) {
|
||||||
|
// При изменении зума принудительно обновляем все маркеры
|
||||||
|
forceRefreshMarkersOnZoomChange();
|
||||||
|
} else {
|
||||||
|
// При повороте только проверяем валидность маркеров
|
||||||
|
checkAndRestoreMarkers();
|
||||||
|
}
|
||||||
lastUpdateTime = currentTime;
|
lastUpdateTime = currentTime;
|
||||||
|
lastZoom = currentZoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -26,11 +26,17 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
private MapObjectCollection mapObjects;
|
private MapObjectCollection mapObjects;
|
||||||
private com.yandex.mapkit.mapview.MapView mapView;
|
private com.yandex.mapkit.mapview.MapView mapView;
|
||||||
private MapInterface.MarkerClickListener markerClickListener;
|
private MapInterface.MarkerClickListener markerClickListener;
|
||||||
|
private com.grigowashere.aismap.utils.SettingsManager settingsManager;
|
||||||
|
|
||||||
// Кеш маркеров с управлением жизненным циклом
|
// Кеш маркеров с управлением жизненным циклом
|
||||||
private Map<String, YandexMarkerWrapper> markerCache = new ConcurrentHashMap<>();
|
private Map<String, YandexMarkerWrapper> markerCache = new ConcurrentHashMap<>();
|
||||||
private YandexMarkerWrapper ownVesselMarker;
|
private YandexMarkerWrapper ownVesselMarker;
|
||||||
|
|
||||||
|
// Трекеры путей движения судов
|
||||||
|
private Map<String, VesselPathTracker> pathTrackers = new ConcurrentHashMap<>();
|
||||||
|
private VesselPathTracker ownVesselPathTracker;
|
||||||
|
private boolean pathTrackingEnabled = true;
|
||||||
|
|
||||||
// Периодическая очистка устаревших маркеров
|
// Периодическая очистка устаревших маркеров
|
||||||
private Handler cleanupHandler;
|
private Handler cleanupHandler;
|
||||||
private Runnable cleanupRunnable;
|
private Runnable cleanupRunnable;
|
||||||
@@ -41,10 +47,11 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
private Runnable refreshRunnable;
|
private Runnable refreshRunnable;
|
||||||
private static final long REFRESH_INTERVAL = 2000; // 2 секунды
|
private static final long REFRESH_INTERVAL = 2000; // 2 секунды
|
||||||
|
|
||||||
public YandexMarkerManager(Context context, MapObjectCollection mapObjects, com.yandex.mapkit.mapview.MapView mapView) {
|
public YandexMarkerManager(Context context, MapObjectCollection mapObjects, com.yandex.mapkit.mapview.MapView mapView, com.grigowashere.aismap.utils.SettingsManager settingsManager) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mapObjects = mapObjects;
|
this.mapObjects = mapObjects;
|
||||||
this.mapView = mapView;
|
this.mapView = mapView;
|
||||||
|
this.settingsManager = settingsManager;
|
||||||
this.cleanupHandler = new Handler(Looper.getMainLooper());
|
this.cleanupHandler = new Handler(Looper.getMainLooper());
|
||||||
this.refreshHandler = new Handler(Looper.getMainLooper());
|
this.refreshHandler = new Handler(Looper.getMainLooper());
|
||||||
}
|
}
|
||||||
@@ -53,6 +60,17 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
public void initialize() {
|
public void initialize() {
|
||||||
startPeriodicCleanup();
|
startPeriodicCleanup();
|
||||||
startPeriodicRefresh();
|
startPeriodicRefresh();
|
||||||
|
|
||||||
|
// Инициализируем настройки путей из SettingsManager
|
||||||
|
if (settingsManager != null) {
|
||||||
|
pathTrackingEnabled = settingsManager.isPathTrackingEnabled();
|
||||||
|
updatePathSettings(
|
||||||
|
settingsManager.getPathColor(),
|
||||||
|
settingsManager.getPredictionColor(),
|
||||||
|
settingsManager.getPathWidth(),
|
||||||
|
settingsManager.getPredictionWidth()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -70,6 +88,17 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
ownVesselMarker.remove();
|
ownVesselMarker.remove();
|
||||||
ownVesselMarker = null;
|
ownVesselMarker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Очищаем трекеры путей
|
||||||
|
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||||
|
tracker.remove();
|
||||||
|
}
|
||||||
|
pathTrackers.clear();
|
||||||
|
|
||||||
|
if (ownVesselPathTracker != null) {
|
||||||
|
ownVesselPathTracker.remove();
|
||||||
|
ownVesselPathTracker = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -90,7 +119,7 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаем новый маркер
|
// Создаем новый маркер
|
||||||
ownVesselMarker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, "own_vessel");
|
ownVesselMarker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, "own_vessel", settingsManager);
|
||||||
if (markerClickListener != null) {
|
if (markerClickListener != null) {
|
||||||
ownVesselMarker.setClickListener(() -> {
|
ownVesselMarker.setClickListener(() -> {
|
||||||
if (markerClickListener != null) {
|
if (markerClickListener != null) {
|
||||||
@@ -98,6 +127,9 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем трекер пути для собственного судна
|
||||||
|
updateOwnVesselPath(vessel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -121,7 +153,7 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Создаем новый маркер
|
// Создаем новый маркер
|
||||||
marker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, mmsi);
|
marker = new YandexMarkerWrapper(context, mapObjects, mapView, vessel, mmsi, settingsManager);
|
||||||
markerCache.put(mmsi, marker);
|
markerCache.put(mmsi, marker);
|
||||||
|
|
||||||
if (markerClickListener != null) {
|
if (markerClickListener != null) {
|
||||||
@@ -131,6 +163,9 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обновляем трекер пути для AIS судна
|
||||||
|
updateAISVesselPath(vessel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -139,6 +174,12 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
if (marker != null) {
|
if (marker != null) {
|
||||||
marker.remove();
|
marker.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем трекер пути
|
||||||
|
VesselPathTracker pathTracker = pathTrackers.remove(mmsi);
|
||||||
|
if (pathTracker != null) {
|
||||||
|
pathTracker.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -147,6 +188,12 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
marker.remove();
|
marker.remove();
|
||||||
}
|
}
|
||||||
markerCache.clear();
|
markerCache.clear();
|
||||||
|
|
||||||
|
// Очищаем все трекеры путей AIS судов
|
||||||
|
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||||
|
tracker.remove();
|
||||||
|
}
|
||||||
|
pathTrackers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -177,6 +224,9 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
public void refreshAllMarkers() {
|
public void refreshAllMarkers() {
|
||||||
// При повороте карты пересоздаем все маркеры
|
// При повороте карты пересоздаем все маркеры
|
||||||
// Это гарантирует правильную ориентацию относительно севера
|
// Это гарантирует правильную ориентацию относительно севера
|
||||||
|
if (mapObjects == null || mapView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Пересоздаем маркер нашего судна
|
// Пересоздаем маркер нашего судна
|
||||||
if (ownVesselMarker != null) {
|
if (ownVesselMarker != null) {
|
||||||
@@ -193,7 +243,11 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
YandexMarkerWrapper marker = entry.getValue();
|
YandexMarkerWrapper marker = entry.getValue();
|
||||||
AISVessel vessel = marker.getAISVessel();
|
AISVessel vessel = marker.getAISVessel();
|
||||||
if (vessel != null) {
|
if (vessel != null) {
|
||||||
|
try {
|
||||||
marker.remove();
|
marker.remove();
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Игнорируем, если underlying объект недоступен
|
||||||
|
}
|
||||||
vesselsToRecreate.put(entry.getKey(), vessel);
|
vesselsToRecreate.put(entry.getKey(), vessel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,7 +255,11 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
// Очищаем кеш и пересоздаем маркеры
|
// Очищаем кеш и пересоздаем маркеры
|
||||||
markerCache.clear();
|
markerCache.clear();
|
||||||
for (Map.Entry<String, AISVessel> entry : vesselsToRecreate.entrySet()) {
|
for (Map.Entry<String, AISVessel> entry : vesselsToRecreate.entrySet()) {
|
||||||
|
try {
|
||||||
updateAISVesselMarker(entry.getValue());
|
updateAISVesselMarker(entry.getValue());
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
// Пропускаем пересоздание при ошибке
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +342,12 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
refreshAllMarkers();
|
refreshAllMarkers();
|
||||||
|
try {
|
||||||
|
// Проверяем только валидность маркеров, не пересоздаем их
|
||||||
|
checkAndRestoreMarkers();
|
||||||
|
} catch (Exception e) {
|
||||||
|
android.util.Log.e(TAG, "Ошибка при периодическом обновлении маркеров: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
// Планируем следующее обновление
|
// Планируем следующее обновление
|
||||||
refreshHandler.postDelayed(this, REFRESH_INTERVAL);
|
refreshHandler.postDelayed(this, REFRESH_INTERVAL);
|
||||||
}
|
}
|
||||||
@@ -309,20 +373,262 @@ public class YandexMarkerManager implements MarkerManager {
|
|||||||
Set<String> toRemove = new HashSet<>();
|
Set<String> toRemove = new HashSet<>();
|
||||||
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
||||||
YandexMarkerWrapper marker = entry.getValue();
|
YandexMarkerWrapper marker = entry.getValue();
|
||||||
if (marker.isExpired() || !marker.isValid()) {
|
if (marker.isExpired() || !marker.isValid() || marker.shouldBeRemoved()) {
|
||||||
marker.remove();
|
marker.remove();
|
||||||
toRemove.add(entry.getKey());
|
toRemove.add(entry.getKey());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Удаляем маркеры и их трекеры путей
|
||||||
for (String mmsi : toRemove) {
|
for (String mmsi : toRemove) {
|
||||||
markerCache.remove(mmsi);
|
markerCache.remove(mmsi);
|
||||||
|
|
||||||
|
// Удаляем трекер пути для этого судна
|
||||||
|
VesselPathTracker pathTracker = pathTrackers.remove(mmsi);
|
||||||
|
if (pathTracker != null) {
|
||||||
|
pathTracker.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Проверяем маркер нашего судна
|
// Проверяем маркер нашего судна
|
||||||
if (ownVesselMarker != null && (ownVesselMarker.isExpired() || !ownVesselMarker.isValid())) {
|
if (ownVesselMarker != null && (ownVesselMarker.isExpired() || !ownVesselMarker.isValid())) {
|
||||||
ownVesselMarker.remove();
|
ownVesselMarker.remove();
|
||||||
ownVesselMarker = null;
|
ownVesselMarker = null;
|
||||||
|
|
||||||
|
// Удаляем трекер пути нашего судна
|
||||||
|
if (ownVesselPathTracker != null) {
|
||||||
|
ownVesselPathTracker.remove();
|
||||||
|
ownVesselPathTracker = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Принудительно обновляет все маркеры (например, при изменении зума)
|
||||||
|
*/
|
||||||
|
public void forceRefreshAllMarkers() {
|
||||||
|
// Пересоздаем маркер нашего судна
|
||||||
|
if (ownVesselMarker != null) {
|
||||||
|
Vessel vessel = ownVesselMarker.getVessel();
|
||||||
|
if (vessel != null) {
|
||||||
|
ownVesselMarker.remove();
|
||||||
|
updateOwnVesselMarker(vessel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пересоздаем все AIS маркеры
|
||||||
|
Map<String, AISVessel> vesselsToRecreate = new HashMap<>();
|
||||||
|
for (Map.Entry<String, YandexMarkerWrapper> entry : markerCache.entrySet()) {
|
||||||
|
YandexMarkerWrapper marker = entry.getValue();
|
||||||
|
AISVessel vessel = marker.getAISVessel();
|
||||||
|
if (vessel != null) {
|
||||||
|
marker.remove();
|
||||||
|
vesselsToRecreate.put(entry.getKey(), vessel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем кеш и трекеры путей
|
||||||
|
markerCache.clear();
|
||||||
|
|
||||||
|
// Очищаем все трекеры путей AIS судов
|
||||||
|
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||||
|
tracker.remove();
|
||||||
|
}
|
||||||
|
pathTrackers.clear();
|
||||||
|
|
||||||
|
// Пересоздаем маркеры
|
||||||
|
for (Map.Entry<String, AISVessel> entry : vesselsToRecreate.entrySet()) {
|
||||||
|
updateAISVesselMarker(entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPathTrackingEnabled(boolean enabled) {
|
||||||
|
this.pathTrackingEnabled = enabled;
|
||||||
|
|
||||||
|
// Сохраняем настройку в SettingsManager
|
||||||
|
if (settingsManager != null) {
|
||||||
|
settingsManager.setPathTrackingEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем состояние всех трекеров
|
||||||
|
if (ownVesselPathTracker != null) {
|
||||||
|
ownVesselPathTracker.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||||
|
tracker.setEnabled(enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearVesselPath(String mmsi) {
|
||||||
|
VesselPathTracker tracker = pathTrackers.get(mmsi);
|
||||||
|
if (tracker != null) {
|
||||||
|
tracker.clearPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearAllPaths() {
|
||||||
|
if (ownVesselPathTracker != null) {
|
||||||
|
ownVesselPathTracker.clearPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||||
|
tracker.clearPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updatePathSettings(int pathColor, int predictionColor, float pathWidth, float predictionWidth) {
|
||||||
|
// Сохраняем настройки в SettingsManager
|
||||||
|
if (settingsManager != null) {
|
||||||
|
settingsManager.setPathColor(pathColor);
|
||||||
|
settingsManager.setPredictionColor(predictionColor);
|
||||||
|
settingsManager.setPathWidth(pathWidth);
|
||||||
|
settingsManager.setPredictionWidth(predictionWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем настройки всех трекеров
|
||||||
|
if (ownVesselPathTracker != null) {
|
||||||
|
ownVesselPathTracker.setPathColor(pathColor);
|
||||||
|
ownVesselPathTracker.setPredictionColor(predictionColor);
|
||||||
|
ownVesselPathTracker.setLineWidth(pathWidth, predictionWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (VesselPathTracker tracker : pathTrackers.values()) {
|
||||||
|
tracker.setPathColor(pathColor);
|
||||||
|
tracker.setPredictionColor(predictionColor);
|
||||||
|
tracker.setLineWidth(pathWidth, predictionWidth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет трекер пути для собственного судна
|
||||||
|
*/
|
||||||
|
private void updateOwnVesselPath(Vessel vessel) {
|
||||||
|
if (!pathTrackingEnabled || vessel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, движется ли судно
|
||||||
|
if (!isVesselMoving(vessel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ownVesselPathTracker == null) {
|
||||||
|
ownVesselPathTracker = new VesselPathTracker("own_vessel", mapObjects);
|
||||||
|
}
|
||||||
|
|
||||||
|
ownVesselPathTracker.updatePosition(
|
||||||
|
vessel.getLatitude(),
|
||||||
|
vessel.getLongitude(),
|
||||||
|
vessel.getSpeed(),
|
||||||
|
vessel.getCourse()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обновляет трекер пути для AIS судна
|
||||||
|
*/
|
||||||
|
private void updateAISVesselPath(AISVessel vessel) {
|
||||||
|
if (!pathTrackingEnabled || vessel == null || vessel.getMmsi() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, движется ли судно
|
||||||
|
if (!isAISVesselMoving(vessel)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String mmsi = vessel.getMmsi();
|
||||||
|
VesselPathTracker tracker = pathTrackers.get(mmsi);
|
||||||
|
|
||||||
|
if (tracker == null) {
|
||||||
|
tracker = new VesselPathTracker(mmsi, mapObjects);
|
||||||
|
pathTrackers.put(mmsi, tracker);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Курс для прогноза: HDG (0..359) если валиден, иначе COG
|
||||||
|
double displayCourse = getAISDisplayCourse(vessel);
|
||||||
|
|
||||||
|
tracker.updatePosition(
|
||||||
|
vessel.getLatitude(),
|
||||||
|
vessel.getLongitude(),
|
||||||
|
vessel.getSpeed(),
|
||||||
|
displayCourse
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, движется ли собственное судно
|
||||||
|
*/
|
||||||
|
private boolean isVesselMoving(Vessel vessel) {
|
||||||
|
if (vessel == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считаем, что судно движется, если скорость больше 0.5 узла
|
||||||
|
return vessel.getSpeed() > 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, движется ли AIS судно
|
||||||
|
*/
|
||||||
|
private boolean isAISVesselMoving(AISVessel vessel) {
|
||||||
|
if (vessel == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем навигационный статус
|
||||||
|
String navStatus = vessel.getNavigationalStatus();
|
||||||
|
if (navStatus != null) {
|
||||||
|
String status = navStatus.toLowerCase();
|
||||||
|
// Считаем, что судно движется, если не стоит на якоре и не пришвартовано
|
||||||
|
if (status.contains("at anchor") ||
|
||||||
|
status.contains("moored") ||
|
||||||
|
status.contains("not under command")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Считаем, что судно движется, если скорость больше 0.5 узла
|
||||||
|
return vessel.getSpeed() > 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает курс для AIS: валидный HDG (0..359), 511 — невалидно; иначе COG
|
||||||
|
*/
|
||||||
|
private double getAISDisplayCourse(AISVessel vessel) {
|
||||||
|
try {
|
||||||
|
double hdg = vessel.getHeading();
|
||||||
|
if (isValidHeading(hdg)) {
|
||||||
|
return normalizeCourse(hdg);
|
||||||
|
}
|
||||||
|
return normalizeCourse(vessel.getCourse());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет валидность HDG
|
||||||
|
*/
|
||||||
|
private boolean isValidHeading(double heading) {
|
||||||
|
if (Double.isNaN(heading) || Double.isInfinite(heading)) return false;
|
||||||
|
int h = (int) Math.round(heading);
|
||||||
|
if (h == 511) return false;
|
||||||
|
return h >= 0 && h <= 359;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нормализует курс в диапазон [0, 360)
|
||||||
|
*/
|
||||||
|
private double normalizeCourse(double course) {
|
||||||
|
if (Double.isNaN(course) || Double.isInfinite(course)) return 0.0;
|
||||||
|
double c = course % 360.0;
|
||||||
|
if (c < 0) c += 360.0;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,28 +44,44 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
private double cachedIconCourse = Double.NaN;
|
private double cachedIconCourse = Double.NaN;
|
||||||
private int cachedIconColor = -1;
|
private int cachedIconColor = -1;
|
||||||
private boolean cachedIconSelected = false;
|
private boolean cachedIconSelected = false;
|
||||||
|
private float cachedIconZoom = -1;
|
||||||
|
private boolean cachedIconStale = false;
|
||||||
|
|
||||||
|
// Ссылка на SettingsManager для получения настроек устаревания
|
||||||
|
private com.grigowashere.aismap.utils.SettingsManager settingsManager;
|
||||||
|
|
||||||
|
// Константы для масштабирования маркеров
|
||||||
|
private static final float MIN_MARKER_SIZE = 24f; // Минимальный размер маркера в пикселях (увеличен)
|
||||||
|
private static final float MAX_MARKER_SIZE = 200f; // Максимальный размер маркера в пикселях (увеличен)
|
||||||
|
private static final float ZOOM_THRESHOLD_FOR_REAL_SIZE = 12f; // Зум, при котором начинаем использовать реальные размеры (снижен)
|
||||||
|
private static final float MEDIUM_ZOOM_SIZE = 48f; // Размер маркера на среднем приближении
|
||||||
|
private static final float CLOSE_ZOOM_SIZE = 80f; // Размер маркера на близком приближении
|
||||||
|
|
||||||
public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects,
|
public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects,
|
||||||
com.yandex.mapkit.mapview.MapView mapView, Vessel vessel, String id) {
|
com.yandex.mapkit.mapview.MapView mapView, Vessel vessel, String id,
|
||||||
|
com.grigowashere.aismap.utils.SettingsManager settingsManager) {
|
||||||
super(id);
|
super(id);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mapObjects = mapObjects;
|
this.mapObjects = mapObjects;
|
||||||
this.mapView = mapView;
|
this.mapView = mapView;
|
||||||
this.vessel = vessel;
|
this.vessel = vessel;
|
||||||
this.isOwnVessel = true;
|
this.isOwnVessel = true;
|
||||||
|
this.settingsManager = settingsManager;
|
||||||
// Предварительно создаем иконку
|
// Предварительно создаем иконку
|
||||||
preloadIcon();
|
preloadIcon();
|
||||||
createMarker();
|
createMarker();
|
||||||
}
|
}
|
||||||
|
|
||||||
public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects,
|
public YandexMarkerWrapper(Context context, MapObjectCollection mapObjects,
|
||||||
com.yandex.mapkit.mapview.MapView mapView, AISVessel vessel, String id) {
|
com.yandex.mapkit.mapview.MapView mapView, AISVessel vessel, String id,
|
||||||
|
com.grigowashere.aismap.utils.SettingsManager settingsManager) {
|
||||||
super(id);
|
super(id);
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.mapObjects = mapObjects;
|
this.mapObjects = mapObjects;
|
||||||
this.mapView = mapView;
|
this.mapView = mapView;
|
||||||
this.aisVessel = vessel;
|
this.aisVessel = vessel;
|
||||||
this.isOwnVessel = false;
|
this.isOwnVessel = false;
|
||||||
|
this.settingsManager = settingsManager;
|
||||||
// Предварительно создаем иконку
|
// Предварительно создаем иконку
|
||||||
preloadIcon();
|
preloadIcon();
|
||||||
createMarker();
|
createMarker();
|
||||||
@@ -76,14 +92,17 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
*/
|
*/
|
||||||
private void preloadIcon() {
|
private void preloadIcon() {
|
||||||
try {
|
try {
|
||||||
double course = isOwnVessel ? vessel.getCourse() : aisVessel.getCourse();
|
// Курс для поворота: HDG (0..359) если валиден, иначе COG
|
||||||
|
double course = getDisplayCourse();
|
||||||
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||||
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||||
|
boolean stale = isDataStale(); // Проверяем устаревание данных
|
||||||
|
|
||||||
cachedIconBitmap = createRotatedIcon(course, color, selected);
|
cachedIconBitmap = createRotatedIcon(course, color, selected, stale);
|
||||||
cachedIconCourse = course;
|
cachedIconCourse = course;
|
||||||
cachedIconColor = color;
|
cachedIconColor = color;
|
||||||
cachedIconSelected = selected;
|
cachedIconSelected = selected;
|
||||||
|
cachedIconStale = stale;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Ошибка предварительной загрузки иконки
|
// Ошибка предварительной загрузки иконки
|
||||||
cachedIconBitmap = null;
|
cachedIconBitmap = null;
|
||||||
@@ -122,11 +141,13 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
*/
|
*/
|
||||||
private Bitmap createIconBitmap() {
|
private Bitmap createIconBitmap() {
|
||||||
try {
|
try {
|
||||||
double course = isOwnVessel ? vessel.getCourse() : aisVessel.getCourse();
|
// Курс для поворота: HDG (0..359) если валиден, иначе COG
|
||||||
|
double course = getDisplayCourse();
|
||||||
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||||
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||||
|
boolean stale = isDataStale(); // Проверяем устаревание данных
|
||||||
|
|
||||||
return createRotatedIcon(course, color, selected);
|
return createRotatedIcon(course, color, selected, stale);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -145,27 +166,36 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
*/
|
*/
|
||||||
private void setIconImmediately() {
|
private void setIconImmediately() {
|
||||||
try {
|
try {
|
||||||
double course = isOwnVessel ? vessel.getCourse() : aisVessel.getCourse();
|
// Курс для поворота: HDG (0..359) если валиден, иначе COG
|
||||||
|
double course = getDisplayCourse();
|
||||||
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
int color = isOwnVessel ? android.graphics.Color.BLUE : getVesselColor();
|
||||||
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
boolean selected = !isOwnVessel && aisVessel.isSelected();
|
||||||
|
boolean stale = isDataStale(); // Проверяем устаревание данных
|
||||||
|
|
||||||
|
// Получаем текущий зум для проверки кеша
|
||||||
|
float currentZoom = getCurrentZoom();
|
||||||
|
|
||||||
// Проверяем кеш иконки
|
// Проверяем кеш иконки
|
||||||
Bitmap iconBitmap = null;
|
Bitmap iconBitmap = null;
|
||||||
if (Double.compare(course, cachedIconCourse) == 0 &&
|
if (Double.compare(course, cachedIconCourse) == 0 &&
|
||||||
color == cachedIconColor &&
|
color == cachedIconColor &&
|
||||||
selected == cachedIconSelected &&
|
selected == cachedIconSelected &&
|
||||||
|
stale == cachedIconStale &&
|
||||||
|
Float.compare(currentZoom, cachedIconZoom) == 0 &&
|
||||||
cachedIconBitmap != null) {
|
cachedIconBitmap != null) {
|
||||||
// Используем кешированную иконку
|
// Используем кешированную иконку
|
||||||
iconBitmap = cachedIconBitmap;
|
iconBitmap = cachedIconBitmap;
|
||||||
} else {
|
} else {
|
||||||
// Создаем новую иконку
|
// Создаем новую иконку
|
||||||
iconBitmap = createRotatedIcon(course, color, selected);
|
iconBitmap = createRotatedIcon(course, color, selected, stale);
|
||||||
if (iconBitmap != null) {
|
if (iconBitmap != null) {
|
||||||
// Кешируем иконку
|
// Кешируем иконку
|
||||||
cachedIconBitmap = iconBitmap;
|
cachedIconBitmap = iconBitmap;
|
||||||
cachedIconCourse = course;
|
cachedIconCourse = course;
|
||||||
cachedIconColor = color;
|
cachedIconColor = color;
|
||||||
cachedIconSelected = selected;
|
cachedIconSelected = selected;
|
||||||
|
cachedIconStale = stale;
|
||||||
|
cachedIconZoom = currentZoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,38 +297,55 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap createRotatedIcon(double course, int color, boolean isSelected) {
|
private Bitmap createRotatedIcon(double course, int color, boolean isSelected, boolean isStale) {
|
||||||
|
// Получаем текущий зум карты
|
||||||
|
float currentZoom = getCurrentZoom();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Получаем drawable из ресурса
|
// Сначала выбираем базовую иконку: для AIS Class A используем targetclassa
|
||||||
int iconResId = context.getResources().getIdentifier("target", "drawable", context.getPackageName());
|
String baseIconName = (!isOwnVessel && isAISClassA()) ? "targetclassa" : "target";
|
||||||
if (iconResId == 0) {
|
int targetIconResId = context.getResources().getIdentifier(baseIconName, "drawable", context.getPackageName());
|
||||||
return createSimpleIcon(color, course);
|
if (targetIconResId == 0) {
|
||||||
|
return createSimpleIcon(color, course, currentZoom, isStale);
|
||||||
}
|
}
|
||||||
|
|
||||||
Drawable drawable = context.getResources().getDrawable(iconResId, null);
|
Drawable targetDrawable = context.getResources().getDrawable(targetIconResId, null);
|
||||||
if (drawable == null) {
|
if (targetDrawable == null) {
|
||||||
return createSimpleIcon(color, course);
|
return createSimpleIcon(color, course, currentZoom, isStale);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Применяем цвет
|
// Получаем иконку losingtarget для наложения (если данные устарели)
|
||||||
|
Drawable losingTargetDrawable = null;
|
||||||
|
if (isStale) {
|
||||||
|
int losingTargetIconResId = context.getResources().getIdentifier("losingtarget", "drawable", context.getPackageName());
|
||||||
|
if (losingTargetIconResId != 0) {
|
||||||
|
losingTargetDrawable = context.getResources().getDrawable(losingTargetIconResId, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Применяем цвет к основной иконке
|
||||||
if (color != 0) {
|
if (color != 0) {
|
||||||
drawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN);
|
targetDrawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Получаем размеры
|
// Получаем размеры основной иконки
|
||||||
int originalWidth = drawable.getIntrinsicWidth();
|
int originalWidth = targetDrawable.getIntrinsicWidth();
|
||||||
int originalHeight = drawable.getIntrinsicHeight();
|
int originalHeight = targetDrawable.getIntrinsicHeight();
|
||||||
|
|
||||||
if (originalWidth <= 0) originalWidth = 32;
|
if (originalWidth <= 0) originalWidth = 32;
|
||||||
if (originalHeight <= 0) originalHeight = 48;
|
if (originalHeight <= 0) originalHeight = 48;
|
||||||
|
|
||||||
// Масштабируем
|
// Рассчитываем размер маркера на основе зума и размеров судна
|
||||||
float scale = 0.3f;
|
float markerSize = calculateMarkerSize(currentZoom);
|
||||||
|
|
||||||
|
// Масштабируем пропорционально рассчитанному размеру
|
||||||
|
float scale = markerSize / Math.max(originalWidth, originalHeight);
|
||||||
int width = (int) (originalWidth * scale);
|
int width = (int) (originalWidth * scale);
|
||||||
int height = (int) (originalHeight * scale);
|
int height = (int) (originalHeight * scale);
|
||||||
|
|
||||||
// Создаем bitmap
|
// Создаем bitmap с дополнительным пространством для обводки и тени
|
||||||
int bitmapSize = Math.max(width, height) + 8;
|
int padding = 12;
|
||||||
|
int bitmapSize = Math.max(width, height) + padding * 2;
|
||||||
Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
|
Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
|
||||||
Canvas canvas = new Canvas(bitmap);
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
|
||||||
@@ -313,7 +360,7 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
// Не удалось получить азимут карты, используем 0
|
// Не удалось получить азимут карты, используем 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Поворачиваем маркер на курс судна с учетом поворота карты
|
// Поворачиваем основную иконку на курс судна с учетом поворота карты
|
||||||
// Курс судна - это направление относительно севера
|
// Курс судна - это направление относительно севера
|
||||||
// Азимут карты - это поворот карты относительно севера
|
// Азимут карты - это поворот карты относительно севера
|
||||||
// Итоговый поворот = курс судна - азимут карты (чтобы маркер оставался относительно севера)
|
// Итоговый поворот = курс судна - азимут карты (чтобы маркер оставался относительно севера)
|
||||||
@@ -324,13 +371,31 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
int left = centerX - width / 2;
|
int left = centerX - width / 2;
|
||||||
int top = centerY - height / 2;
|
int top = centerY - height / 2;
|
||||||
|
|
||||||
drawable.setBounds(left, top, left + width, top + height);
|
// Рисуем тень (смещенную копию)
|
||||||
|
targetDrawable.setBounds(left + 2, top + 2, left + width + 2, top + height + 2);
|
||||||
|
targetDrawable.setColorFilter(0x80000000, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||||
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.rotate(rotationAngle, centerX, centerY);
|
canvas.rotate(rotationAngle, centerX, centerY);
|
||||||
drawable.draw(canvas);
|
targetDrawable.draw(canvas);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
|
||||||
|
// Рисуем основную иконку target (поворачивается)
|
||||||
|
targetDrawable.setBounds(left, top, left + width, top + height);
|
||||||
|
targetDrawable.setColorFilter(color, android.graphics.PorterDuff.Mode.SRC_IN);
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.rotate(rotationAngle, centerX, centerY);
|
||||||
|
targetDrawable.draw(canvas);
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
// Рисуем losingtarget поверх (НЕ поворачивается)
|
||||||
|
if (losingTargetDrawable != null) {
|
||||||
|
// Используем тот же размер для losingtarget
|
||||||
|
losingTargetDrawable.setBounds(left, top, left + width, top + height);
|
||||||
|
losingTargetDrawable.draw(canvas); // Без поворота!
|
||||||
|
}
|
||||||
|
|
||||||
// Добавляем рамку выделения если нужно
|
// Добавляем рамку выделения если нужно
|
||||||
if (isSelected) {
|
if (isSelected) {
|
||||||
addSelectionFrame(canvas, centerX, centerY, Math.max(width, height));
|
addSelectionFrame(canvas, centerX, centerY, Math.max(width, height));
|
||||||
@@ -338,31 +403,139 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return createSimpleIcon(color, course);
|
return createSimpleIcon(color, course, currentZoom, isStale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap createSimpleIcon(int color, double course) {
|
/**
|
||||||
|
* Возвращает курс для отображения маркера: валидный HDG (0..359), иначе COG
|
||||||
|
*/
|
||||||
|
private double getDisplayCourse() {
|
||||||
try {
|
try {
|
||||||
int size = 32;
|
if (isOwnVessel) {
|
||||||
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
double cog = vessel != null ? vessel.getCourse() : 0.0;
|
||||||
|
return normalizeCourse(cog);
|
||||||
|
}
|
||||||
|
if (aisVessel != null) {
|
||||||
|
double hdg = aisVessel.getHeading();
|
||||||
|
if (isValidHeading(hdg)) {
|
||||||
|
return normalizeCourse(hdg);
|
||||||
|
}
|
||||||
|
double cog = aisVessel.getCourse();
|
||||||
|
return normalizeCourse(cog);
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверка валидности HDG: 0..359 включительно, 511 — невалидно
|
||||||
|
*/
|
||||||
|
private boolean isValidHeading(double heading) {
|
||||||
|
if (Double.isNaN(heading) || Double.isInfinite(heading)) return false;
|
||||||
|
int h = (int) Math.round(heading);
|
||||||
|
if (h == 511) return false;
|
||||||
|
return h >= 0 && h <= 359;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нормализует курс к диапазону [0, 360)
|
||||||
|
*/
|
||||||
|
private double normalizeCourse(double course) {
|
||||||
|
if (Double.isNaN(course) || Double.isInfinite(course)) return 0.0;
|
||||||
|
double c = course % 360.0;
|
||||||
|
if (c < 0) c += 360.0;
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isAISClassA() {
|
||||||
|
try {
|
||||||
|
if (aisVessel == null) return false;
|
||||||
|
String cls = aisVessel.getVesselClass();
|
||||||
|
if (cls == null) return false;
|
||||||
|
String s = cls.trim().toLowerCase();
|
||||||
|
return s.equals("class a") || s.equals("a") || s.contains("class a");
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap createSimpleIcon(int color, double course, float zoom, boolean isStale) {
|
||||||
|
try {
|
||||||
|
// Рассчитываем размер маркера на основе зума
|
||||||
|
float markerSize = calculateMarkerSize(zoom);
|
||||||
|
int size = (int) markerSize;
|
||||||
|
|
||||||
|
// Увеличиваем размер bitmap для обводки и тени
|
||||||
|
int padding = 8;
|
||||||
|
int bitmapSize = size + padding * 2;
|
||||||
|
Bitmap bitmap = Bitmap.createBitmap(bitmapSize, bitmapSize, Bitmap.Config.ARGB_8888);
|
||||||
Canvas canvas = new Canvas(bitmap);
|
Canvas canvas = new Canvas(bitmap);
|
||||||
|
|
||||||
Paint paint = new Paint();
|
// Смещаем координаты с учетом padding
|
||||||
paint.setColor(color);
|
float centerX = bitmapSize / 2f;
|
||||||
paint.setStyle(Paint.Style.FILL);
|
float centerY = bitmapSize / 2f;
|
||||||
paint.setAntiAlias(true);
|
|
||||||
|
|
||||||
// Рисуем треугольник
|
// Создаем путь для треугольника
|
||||||
android.graphics.Path path = new android.graphics.Path();
|
android.graphics.Path path = new android.graphics.Path();
|
||||||
path.moveTo(size / 2f, 0);
|
path.moveTo(centerX, padding);
|
||||||
path.lineTo(size * 0.1f, size * 0.8f);
|
path.lineTo(padding + size * 0.1f, padding + size * 0.8f);
|
||||||
path.lineTo(size * 0.9f, size * 0.8f);
|
path.lineTo(padding + size * 0.9f, padding + size * 0.8f);
|
||||||
path.close();
|
path.close();
|
||||||
|
|
||||||
|
// Рисуем тень (смещенную копию)
|
||||||
|
Paint shadowPaint = new Paint();
|
||||||
|
shadowPaint.setColor(0x80000000); // Полупрозрачный черный
|
||||||
|
shadowPaint.setStyle(Paint.Style.FILL);
|
||||||
|
shadowPaint.setAntiAlias(true);
|
||||||
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.rotate((float) course, size / 2f, size / 2f);
|
canvas.translate(2, 2); // Смещение для тени
|
||||||
canvas.drawPath(path, paint);
|
canvas.rotate((float) course, centerX, centerY);
|
||||||
|
canvas.drawPath(path, shadowPaint);
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
// Рисуем внешнюю обводку
|
||||||
|
Paint outlinePaint = new Paint();
|
||||||
|
outlinePaint.setColor(0xFF000000); // Черная обводка
|
||||||
|
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
outlinePaint.setStrokeWidth(4f);
|
||||||
|
outlinePaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.rotate((float) course, centerX, centerY);
|
||||||
|
canvas.drawPath(path, outlinePaint);
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
// Рисуем внутреннюю обводку
|
||||||
|
Paint innerOutlinePaint = new Paint();
|
||||||
|
innerOutlinePaint.setColor(0xFFFFFFFF); // Белая внутренняя обводка
|
||||||
|
innerOutlinePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
innerOutlinePaint.setStrokeWidth(2f);
|
||||||
|
innerOutlinePaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.rotate((float) course, centerX, centerY);
|
||||||
|
canvas.drawPath(path, innerOutlinePaint);
|
||||||
|
canvas.restore();
|
||||||
|
|
||||||
|
// Рисуем основную заливку
|
||||||
|
Paint fillPaint = new Paint();
|
||||||
|
fillPaint.setColor(color);
|
||||||
|
fillPaint.setStyle(Paint.Style.FILL);
|
||||||
|
fillPaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
// Для устаревших данных рисуем пунктирный треугольник
|
||||||
|
if (isStale) {
|
||||||
|
fillPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
fillPaint.setStrokeWidth(3f);
|
||||||
|
fillPaint.setPathEffect(new android.graphics.DashPathEffect(new float[]{10, 5}, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save();
|
||||||
|
canvas.rotate((float) course, centerX, centerY);
|
||||||
|
canvas.drawPath(path, fillPaint);
|
||||||
canvas.restore();
|
canvas.restore();
|
||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
@@ -373,12 +546,41 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
|
|
||||||
private void addSelectionFrame(Canvas canvas, int centerX, int centerY, int size) {
|
private void addSelectionFrame(Canvas canvas, int centerX, int centerY, int size) {
|
||||||
try {
|
try {
|
||||||
|
// Сначала рисуем тень для рамки выделения
|
||||||
|
Paint shadowPaint = new Paint();
|
||||||
|
shadowPaint.setColor(0x80000000);
|
||||||
|
shadowPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
shadowPaint.setStrokeWidth(6f);
|
||||||
|
shadowPaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
int shadowSize = size + 20;
|
||||||
|
canvas.drawCircle(centerX + 2, centerY + 2, shadowSize / 2, shadowPaint);
|
||||||
|
|
||||||
|
// Рисуем внешнюю обводку
|
||||||
|
Paint outerOutlinePaint = new Paint();
|
||||||
|
outerOutlinePaint.setColor(0xFF000000);
|
||||||
|
outerOutlinePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
outerOutlinePaint.setStrokeWidth(4f);
|
||||||
|
outerOutlinePaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
int outerSize = size + 18;
|
||||||
|
canvas.drawCircle(centerX, centerY, outerSize / 2, outerOutlinePaint);
|
||||||
|
|
||||||
|
// Рисуем внутреннюю обводку
|
||||||
|
Paint innerOutlinePaint = new Paint();
|
||||||
|
innerOutlinePaint.setColor(0xFFFFFFFF);
|
||||||
|
innerOutlinePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
innerOutlinePaint.setStrokeWidth(2f);
|
||||||
|
innerOutlinePaint.setAntiAlias(true);
|
||||||
|
|
||||||
|
int innerSize = size + 16;
|
||||||
|
canvas.drawCircle(centerX, centerY, innerSize / 2, innerOutlinePaint);
|
||||||
|
|
||||||
|
// Пробуем использовать иконку chosentarget если доступна
|
||||||
int iconResId = context.getResources().getIdentifier("chosentarget", "drawable", context.getPackageName());
|
int iconResId = context.getResources().getIdentifier("chosentarget", "drawable", context.getPackageName());
|
||||||
if (iconResId == 0) return;
|
if (iconResId != 0) {
|
||||||
|
|
||||||
Drawable selectionDrawable = context.getResources().getDrawable(iconResId, null);
|
Drawable selectionDrawable = context.getResources().getDrawable(iconResId, null);
|
||||||
if (selectionDrawable == null) return;
|
if (selectionDrawable != null) {
|
||||||
|
|
||||||
int selectionSize = size + 16;
|
int selectionSize = size + 16;
|
||||||
int selectionLeft = centerX - selectionSize / 2;
|
int selectionLeft = centerX - selectionSize / 2;
|
||||||
int selectionTop = centerY - selectionSize / 2;
|
int selectionTop = centerY - selectionSize / 2;
|
||||||
@@ -386,6 +588,8 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
selectionDrawable.setBounds(selectionLeft, selectionTop,
|
selectionDrawable.setBounds(selectionLeft, selectionTop,
|
||||||
selectionLeft + selectionSize, selectionTop + selectionSize);
|
selectionLeft + selectionSize, selectionTop + selectionSize);
|
||||||
selectionDrawable.draw(canvas);
|
selectionDrawable.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// Игнорируем ошибки рамки выделения
|
// Игнорируем ошибки рамки выделения
|
||||||
}
|
}
|
||||||
@@ -425,4 +629,93 @@ public class YandexMarkerWrapper extends MarkerWrapper {
|
|||||||
public boolean isOwnVessel() {
|
public boolean isOwnVessel() {
|
||||||
return isOwnVessel;
|
return isOwnVessel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, устарели ли данные судна (для AIS судов)
|
||||||
|
*/
|
||||||
|
public boolean isDataStale() {
|
||||||
|
if (isOwnVessel || aisVessel == null || settingsManager == null) {
|
||||||
|
return false; // Собственное судно никогда не устаревает
|
||||||
|
}
|
||||||
|
return aisVessel.isDataStale(settingsManager.getDataStaleWarningMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, нужно ли удалить судно (для AIS судов)
|
||||||
|
*/
|
||||||
|
public boolean shouldBeRemoved() {
|
||||||
|
if (isOwnVessel || aisVessel == null || settingsManager == null) {
|
||||||
|
return false; // Собственное судно никогда не удаляется
|
||||||
|
}
|
||||||
|
return aisVessel.shouldBeRemoved(settingsManager.getDataStaleRemoveMinutes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает текущий зум карты
|
||||||
|
*/
|
||||||
|
private float getCurrentZoom() {
|
||||||
|
try {
|
||||||
|
if (mapView != null) {
|
||||||
|
com.yandex.mapkit.map.CameraPosition cameraPosition = mapView.getMap().getCameraPosition();
|
||||||
|
return cameraPosition.getZoom();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Ошибка получения зума, возвращаем значение по умолчанию
|
||||||
|
}
|
||||||
|
return 10f; // Значение по умолчанию
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Рассчитывает размер маркера на основе зума и размеров судна
|
||||||
|
*/
|
||||||
|
private float calculateMarkerSize(float zoom) {
|
||||||
|
// На очень большом расстоянии используем минимальный размер
|
||||||
|
if (zoom < 8) {
|
||||||
|
return MIN_MARKER_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// На среднем расстоянии используем средний размер
|
||||||
|
if (zoom < 12) {
|
||||||
|
return MEDIUM_ZOOM_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// На близком расстоянии используем крупный размер
|
||||||
|
if (zoom < 15) {
|
||||||
|
return CLOSE_ZOOM_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// При очень близком приближении рассчитываем размер на основе реальных размеров судна
|
||||||
|
double vesselLength = 0;
|
||||||
|
double vesselWidth = 0;
|
||||||
|
|
||||||
|
if (isOwnVessel && vessel != null) {
|
||||||
|
// Для собственного судна используем примерные размеры
|
||||||
|
vesselLength = 50; // метры
|
||||||
|
vesselWidth = 10; // метры
|
||||||
|
} else if (!isOwnVessel && aisVessel != null) {
|
||||||
|
vesselLength = aisVessel.getLength();
|
||||||
|
vesselWidth = aisVessel.getWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если размеры не заданы или очень маленькие, используем увеличенный базовый размер
|
||||||
|
if (vesselLength <= 0 || vesselWidth <= 0 || vesselLength < 10 || vesselWidth < 5) {
|
||||||
|
// Используем размер, основанный на зуме, но увеличенный
|
||||||
|
float baseSize = CLOSE_ZOOM_SIZE + (zoom - 15) * 8; // Увеличиваем размер с зумом
|
||||||
|
return Math.max(CLOSE_ZOOM_SIZE, Math.min(MAX_MARKER_SIZE, baseSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рассчитываем размер на основе большего из размеров судна
|
||||||
|
double vesselSize = Math.max(vesselLength, vesselWidth);
|
||||||
|
|
||||||
|
// Коэффициент масштабирования (пиксели на метр при текущем зуме)
|
||||||
|
// Чем больше зум, тем больше пикселей на метр
|
||||||
|
float pixelsPerMeter = (float) (Math.pow(2, zoom - 12) * 1.0); // Увеличенный коэффициент
|
||||||
|
|
||||||
|
// Размер маркера в пикселях
|
||||||
|
float calculatedSize = (float) (vesselSize * pixelsPerMeter);
|
||||||
|
|
||||||
|
// Ограничиваем размер маркера, но с более высоким минимумом
|
||||||
|
float minSize = Math.max(CLOSE_ZOOM_SIZE, MIN_MARKER_SIZE);
|
||||||
|
return Math.max(minSize, Math.min(MAX_MARKER_SIZE, calculatedSize));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public class AISVessel {
|
|||||||
private double course; // курс в градусах (0-360)
|
private double course; // курс в градусах (0-360)
|
||||||
private double speed; // скорость в узлах
|
private double speed; // скорость в узлах
|
||||||
private double heading; // направление движения в градусах
|
private double heading; // направление движения в градусах
|
||||||
|
private double rateOfTurn; // скорость поворота в градусах/минуту
|
||||||
private double length; // длина судна в метрах
|
private double length; // длина судна в метрах
|
||||||
private double width; // ширина судна в метрах
|
private double width; // ширина судна в метрах
|
||||||
private double draft; // осадка в метрах
|
private double draft; // осадка в метрах
|
||||||
@@ -72,6 +73,9 @@ public class AISVessel {
|
|||||||
public double getHeading() { return heading; }
|
public double getHeading() { return heading; }
|
||||||
public void setHeading(double heading) { this.heading = heading; }
|
public void setHeading(double heading) { this.heading = heading; }
|
||||||
|
|
||||||
|
public double getRateOfTurn() { return rateOfTurn; }
|
||||||
|
public void setRateOfTurn(double rateOfTurn) { this.rateOfTurn = rateOfTurn; }
|
||||||
|
|
||||||
public double getLength() { return length; }
|
public double getLength() { return length; }
|
||||||
public void setLength(double length) { this.length = length; }
|
public void setLength(double length) { this.length = length; }
|
||||||
|
|
||||||
@@ -126,12 +130,47 @@ public class AISVessel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Проверяет, не устарели ли данные (больше 10 минут)
|
* Обновляет позицию, курс и скорость поворота судна
|
||||||
*/
|
*/
|
||||||
|
public void updatePosition(double latitude, double longitude, double course, double speed, double rateOfTurn) {
|
||||||
|
this.latitude = latitude;
|
||||||
|
this.longitude = longitude;
|
||||||
|
this.course = course;
|
||||||
|
this.speed = speed;
|
||||||
|
this.rateOfTurn = rateOfTurn;
|
||||||
|
this.lastUpdate = LocalDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, не устарели ли данные (больше 10 минут)
|
||||||
|
* @deprecated Используйте isDataStale(int warningMinutes) для настраиваемого времени
|
||||||
|
*/
|
||||||
|
@Deprecated
|
||||||
public boolean isDataStale() {
|
public boolean isDataStale() {
|
||||||
return LocalDateTime.now().minusMinutes(10).isAfter(lastUpdate);
|
return LocalDateTime.now().minusMinutes(10).isAfter(lastUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, не устарели ли данные на указанное количество минут
|
||||||
|
*/
|
||||||
|
public boolean isDataStale(int warningMinutes) {
|
||||||
|
return LocalDateTime.now().minusMinutes(warningMinutes).isAfter(lastUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, нужно ли удалить данные (старше указанного количества минут)
|
||||||
|
*/
|
||||||
|
public boolean shouldBeRemoved(int removeMinutes) {
|
||||||
|
return LocalDateTime.now().minusMinutes(removeMinutes).isAfter(lastUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает количество минут с последнего обновления
|
||||||
|
*/
|
||||||
|
public long getMinutesSinceLastUpdate() {
|
||||||
|
return java.time.Duration.between(lastUpdate, LocalDateTime.now()).toMinutes();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "AISVessel{" +
|
return "AISVessel{" +
|
||||||
@@ -141,6 +180,7 @@ public class AISVessel {
|
|||||||
", lon=" + longitude +
|
", lon=" + longitude +
|
||||||
", course=" + course +
|
", course=" + course +
|
||||||
", speed=" + speed +
|
", speed=" + speed +
|
||||||
|
", rot=" + rateOfTurn +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.grigowashere.aismap.services;
|
||||||
|
|
||||||
|
import android.app.Notification;
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.MainActivity;
|
||||||
|
import com.grigowashere.aismap.R;
|
||||||
|
|
||||||
|
public class AISForegroundService extends Service {
|
||||||
|
|
||||||
|
public static final String CHANNEL_ID = "aismap_foreground";
|
||||||
|
private static final int NOTIFICATION_ID = 1001;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate() {
|
||||||
|
super.onCreate();
|
||||||
|
createNotificationChannel();
|
||||||
|
startForeground(NOTIFICATION_ID, buildNotification("Работа в фоне: обновление AIS/GPS"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
// Здесь в дальнейшем запустим прием NMEA/UDP и GPS слушателей
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createNotificationChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"AISMap Background",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
);
|
||||||
|
channel.setDescription("Фоновые обновления AIS и GPS");
|
||||||
|
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
if (nm != null) nm.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification buildNotification(String content) {
|
||||||
|
Intent notificationIntent = new Intent(this, MainActivity.class);
|
||||||
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT : 0;
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags);
|
||||||
|
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("AISMap")
|
||||||
|
.setContentText(content)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package com.grigowashere.aismap.services;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioManager;
|
||||||
|
import android.media.ToneGenerator;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.VibrationEffect;
|
||||||
|
import android.os.Vibrator;
|
||||||
|
import android.os.VibratorManager;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import androidx.core.app.NotificationManagerCompat;
|
||||||
|
|
||||||
|
import com.grigowashere.aismap.MainActivity;
|
||||||
|
import com.grigowashere.aismap.R;
|
||||||
|
import com.grigowashere.aismap.utils.SettingsManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сервис для обработки уведомлений о новых AIS целях
|
||||||
|
* Поддерживает вибрацию и звуковые уведомления
|
||||||
|
*/
|
||||||
|
public class NotificationService {
|
||||||
|
|
||||||
|
private static final String TAG = "NotificationService";
|
||||||
|
private static final String ALERT_CHANNEL_ID = "aismap_alerts";
|
||||||
|
private static final int SAFETY_NOTIFICATION_ID_BASE = 2000;
|
||||||
|
|
||||||
|
private Context context;
|
||||||
|
private SettingsManager settingsManager;
|
||||||
|
private Vibrator vibrator;
|
||||||
|
private ToneGenerator toneGenerator;
|
||||||
|
private boolean isInitialized = false;
|
||||||
|
|
||||||
|
public NotificationService(Context context) {
|
||||||
|
this.context = context;
|
||||||
|
this.settingsManager = new SettingsManager(context);
|
||||||
|
initializeService();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализирует сервис уведомлений
|
||||||
|
*/
|
||||||
|
private void initializeService() {
|
||||||
|
try {
|
||||||
|
createAlertChannel();
|
||||||
|
// Инициализация вибратора
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
|
||||||
|
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
|
||||||
|
vibrator = vibratorManager.getDefaultVibrator();
|
||||||
|
} else {
|
||||||
|
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Инициализация генератора тонов
|
||||||
|
toneGenerator = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100);
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
Log.i(TAG, "Сервис уведомлений инициализирован успешно");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка инициализации сервиса уведомлений: " + e.getMessage(), e);
|
||||||
|
isInitialized = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Создает канал уведомлений для предупреждений (Android O+)
|
||||||
|
*/
|
||||||
|
private void createAlertChannel() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
ALERT_CHANNEL_ID,
|
||||||
|
"AIS Alerts",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
);
|
||||||
|
channel.setDescription("Сообщения безопасности AIS и предупреждения");
|
||||||
|
NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
if (nm != null) nm.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Воспроизводит уведомление о новой AIS цели
|
||||||
|
*/
|
||||||
|
public void notifyNewAISTarget() {
|
||||||
|
if (!isInitialized) {
|
||||||
|
Log.w(TAG, "Сервис уведомлений не инициализирован");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем настройки и воспроизводим соответствующие уведомления
|
||||||
|
if (settingsManager.isVibrationEnabled()) {
|
||||||
|
playVibration();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsManager.isSoundEnabled()) {
|
||||||
|
playSound();
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.i(TAG, "Уведомление о новой AIS цели воспроизведено");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Воспроизводит вибрацию
|
||||||
|
*/
|
||||||
|
private void playVibration() {
|
||||||
|
try {
|
||||||
|
if (vibrator != null && vibrator.hasVibrator()) {
|
||||||
|
// Паттерн вибрации: короткая пауза, длинная вибрация, короткая пауза, короткая вибрация
|
||||||
|
long[] pattern = {0, 200, 100, 100};
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
|
||||||
|
vibrator.vibrate(effect);
|
||||||
|
} else {
|
||||||
|
vibrator.vibrate(pattern, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "Вибрация воспроизведена");
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Вибратор недоступен");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка воспроизведения вибрации: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Воспроизводит звуковое уведомление
|
||||||
|
*/
|
||||||
|
private void playSound() {
|
||||||
|
try {
|
||||||
|
if (toneGenerator != null) {
|
||||||
|
// Воспроизводим тон уведомления (TONE_CDMA_ALERT_CALL_GUARD)
|
||||||
|
toneGenerator.startTone(ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD, 500);
|
||||||
|
|
||||||
|
Log.d(TAG, "Звуковое уведомление воспроизведено");
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Генератор тонов недоступен");
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка воспроизведения звука: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включены ли уведомления
|
||||||
|
*/
|
||||||
|
public boolean areNotificationsEnabled() {
|
||||||
|
return settingsManager.isVibrationEnabled() || settingsManager.isSoundEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включена ли вибрация
|
||||||
|
*/
|
||||||
|
public boolean isVibrationEnabled() {
|
||||||
|
return settingsManager.isVibrationEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включен ли звук
|
||||||
|
*/
|
||||||
|
public boolean isSoundEnabled() {
|
||||||
|
return settingsManager.isSoundEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Уведомление о сообщении безопасности (AIS 14)
|
||||||
|
*/
|
||||||
|
public void notifySafetyMessage(String mmsi, String text) {
|
||||||
|
if (!isInitialized) {
|
||||||
|
Log.w(TAG, "Сервис уведомлений не инициализирован");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Подаем сигнал по настройкам (вибро/звук)
|
||||||
|
if (settingsManager.isVibrationEnabled()) {
|
||||||
|
playVibration();
|
||||||
|
}
|
||||||
|
if (settingsManager.isSoundEnabled()) {
|
||||||
|
playSound();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Показ системного уведомления в шторке
|
||||||
|
try {
|
||||||
|
createAlertChannel();
|
||||||
|
Intent intent = new Intent(context, MainActivity.class);
|
||||||
|
int flags = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT : 0;
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, flags);
|
||||||
|
|
||||||
|
String title = "AIS Safety message";
|
||||||
|
String content = (text != null && !text.isEmpty()) ? text : ("Сообщение от " + mmsi);
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, ALERT_CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(content)
|
||||||
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(content))
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
|
||||||
|
.setContentIntent(pendingIntent)
|
||||||
|
.setAutoCancel(true);
|
||||||
|
|
||||||
|
int notificationId = SAFETY_NOTIFICATION_ID_BASE + (mmsi != null ? (mmsi.hashCode() & 0x0FFF) : 0);
|
||||||
|
NotificationManagerCompat.from(context).notify(notificationId, builder.build());
|
||||||
|
|
||||||
|
Log.i(TAG, "Показано системное уведомление о safety-сообщении: MMSI=" + mmsi);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка показа системного уведомления: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Освобождает ресурсы сервиса
|
||||||
|
*/
|
||||||
|
public void cleanup() {
|
||||||
|
try {
|
||||||
|
if (toneGenerator != null) {
|
||||||
|
toneGenerator.release();
|
||||||
|
toneGenerator = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vibrator != null) {
|
||||||
|
vibrator.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized = false;
|
||||||
|
Log.i(TAG, "Ресурсы сервиса уведомлений освобождены");
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Ошибка при освобождении ресурсов сервиса уведомлений: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
package com.grigowashere.aismap.utils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Маппинг MID (первые 3 цифры MMSI) -> ISO 3166-1 alpha-2 кода страны
|
||||||
|
*/
|
||||||
|
public final class MIDToCountry {
|
||||||
|
|
||||||
|
public static final Map<String, String> MID_TO_COUNTRY;
|
||||||
|
|
||||||
|
static {
|
||||||
|
MID_TO_COUNTRY = new HashMap<>();
|
||||||
|
// Europe
|
||||||
|
MID_TO_COUNTRY.put("201", "AL"); // Albania
|
||||||
|
MID_TO_COUNTRY.put("202", "AD"); // Andorra
|
||||||
|
MID_TO_COUNTRY.put("203", "AT"); // Austria
|
||||||
|
MID_TO_COUNTRY.put("204", "PT"); // Portugal (Azores)
|
||||||
|
MID_TO_COUNTRY.put("205", "BE"); // Belgium
|
||||||
|
MID_TO_COUNTRY.put("206", "BY"); // Belarus
|
||||||
|
MID_TO_COUNTRY.put("207", "BG"); // Bulgaria
|
||||||
|
MID_TO_COUNTRY.put("208", "VA"); // Vatican City
|
||||||
|
MID_TO_COUNTRY.put("209", "CY"); // Cyprus
|
||||||
|
MID_TO_COUNTRY.put("210", "CY"); // Cyprus
|
||||||
|
MID_TO_COUNTRY.put("211", "DE"); // Germany
|
||||||
|
MID_TO_COUNTRY.put("212", "CY"); // Cyprus
|
||||||
|
MID_TO_COUNTRY.put("213", "GE"); // Georgia
|
||||||
|
MID_TO_COUNTRY.put("214", "MD"); // Moldova
|
||||||
|
MID_TO_COUNTRY.put("215", "MT"); // Malta
|
||||||
|
MID_TO_COUNTRY.put("216", "AM"); // Armenia
|
||||||
|
MID_TO_COUNTRY.put("218", "DE"); // Germany
|
||||||
|
MID_TO_COUNTRY.put("219", "DK"); // Denmark
|
||||||
|
MID_TO_COUNTRY.put("220", "DK"); // Denmark
|
||||||
|
MID_TO_COUNTRY.put("224", "ES"); // Spain
|
||||||
|
MID_TO_COUNTRY.put("225", "ES"); // Spain
|
||||||
|
MID_TO_COUNTRY.put("226", "FR"); // France
|
||||||
|
MID_TO_COUNTRY.put("227", "FR"); // France
|
||||||
|
MID_TO_COUNTRY.put("228", "FR"); // France
|
||||||
|
MID_TO_COUNTRY.put("229", "MT"); // Malta
|
||||||
|
MID_TO_COUNTRY.put("230", "FI"); // Finland
|
||||||
|
MID_TO_COUNTRY.put("231", "FO"); // Faroe Islands
|
||||||
|
MID_TO_COUNTRY.put("232", "GB"); // United Kingdom
|
||||||
|
MID_TO_COUNTRY.put("233", "GB"); // United Kingdom
|
||||||
|
MID_TO_COUNTRY.put("234", "GB"); // United Kingdom
|
||||||
|
MID_TO_COUNTRY.put("235", "GB"); // United Kingdom
|
||||||
|
MID_TO_COUNTRY.put("236", "GI"); // Gibraltar
|
||||||
|
MID_TO_COUNTRY.put("237", "GR"); // Greece
|
||||||
|
MID_TO_COUNTRY.put("238", "HR"); // Croatia
|
||||||
|
MID_TO_COUNTRY.put("239", "GR"); // Greece
|
||||||
|
MID_TO_COUNTRY.put("240", "GR"); // Greece
|
||||||
|
MID_TO_COUNTRY.put("241", "GR"); // Greece
|
||||||
|
MID_TO_COUNTRY.put("242", "MA"); // Morocco
|
||||||
|
MID_TO_COUNTRY.put("243", "HU"); // Hungary
|
||||||
|
MID_TO_COUNTRY.put("244", "NL"); // Netherlands
|
||||||
|
MID_TO_COUNTRY.put("245", "NL"); // Netherlands
|
||||||
|
MID_TO_COUNTRY.put("246", "NL"); // Netherlands
|
||||||
|
MID_TO_COUNTRY.put("247", "IT"); // Italy
|
||||||
|
MID_TO_COUNTRY.put("248", "MT"); // Malta
|
||||||
|
MID_TO_COUNTRY.put("249", "MT"); // Malta
|
||||||
|
MID_TO_COUNTRY.put("250", "IE"); // Ireland
|
||||||
|
MID_TO_COUNTRY.put("251", "IS"); // Iceland
|
||||||
|
MID_TO_COUNTRY.put("252", "LI"); // Liechtenstein
|
||||||
|
MID_TO_COUNTRY.put("253", "LU"); // Luxembourg
|
||||||
|
MID_TO_COUNTRY.put("254", "MC"); // Monaco
|
||||||
|
MID_TO_COUNTRY.put("255", "PT"); // Portugal (Madeira)
|
||||||
|
MID_TO_COUNTRY.put("256", "MT"); // Malta
|
||||||
|
MID_TO_COUNTRY.put("257", "NO"); // Norway
|
||||||
|
MID_TO_COUNTRY.put("258", "NO"); // Norway
|
||||||
|
MID_TO_COUNTRY.put("259", "NO"); // Norway
|
||||||
|
MID_TO_COUNTRY.put("261", "PL"); // Poland
|
||||||
|
MID_TO_COUNTRY.put("262", "ME"); // Montenegro
|
||||||
|
MID_TO_COUNTRY.put("263", "PT"); // Portugal
|
||||||
|
MID_TO_COUNTRY.put("264", "RO"); // Romania
|
||||||
|
MID_TO_COUNTRY.put("265", "SE"); // Sweden
|
||||||
|
MID_TO_COUNTRY.put("266", "SE"); // Sweden
|
||||||
|
MID_TO_COUNTRY.put("267", "SK"); // Slovakia
|
||||||
|
MID_TO_COUNTRY.put("268", "SM"); // San Marino
|
||||||
|
MID_TO_COUNTRY.put("269", "CH"); // Switzerland
|
||||||
|
MID_TO_COUNTRY.put("270", "CZ"); // Czech Republic
|
||||||
|
MID_TO_COUNTRY.put("271", "TR"); // Turkey
|
||||||
|
MID_TO_COUNTRY.put("272", "UA"); // Ukraine
|
||||||
|
MID_TO_COUNTRY.put("273", "RU"); // Russian Federation
|
||||||
|
MID_TO_COUNTRY.put("274", "MK"); // North Macedonia
|
||||||
|
MID_TO_COUNTRY.put("275", "LV"); // Latvia
|
||||||
|
MID_TO_COUNTRY.put("276", "EE"); // Estonia
|
||||||
|
MID_TO_COUNTRY.put("277", "LT"); // Lithuania
|
||||||
|
MID_TO_COUNTRY.put("278", "SI"); // Slovenia
|
||||||
|
MID_TO_COUNTRY.put("279", "RS"); // Serbia
|
||||||
|
|
||||||
|
// North America & Caribbean
|
||||||
|
MID_TO_COUNTRY.put("301", "AI"); // Anguilla
|
||||||
|
MID_TO_COUNTRY.put("303", "US"); // USA (Alaska)
|
||||||
|
MID_TO_COUNTRY.put("304", "AG"); // Antigua and Barbuda
|
||||||
|
MID_TO_COUNTRY.put("305", "AG"); // Antigua and Barbuda
|
||||||
|
MID_TO_COUNTRY.put("306", "CW"); // Curaçao
|
||||||
|
MID_TO_COUNTRY.put("307", "AW"); // Aruba
|
||||||
|
MID_TO_COUNTRY.put("308", "BS"); // Bahamas
|
||||||
|
MID_TO_COUNTRY.put("309", "BS"); // Bahamas
|
||||||
|
MID_TO_COUNTRY.put("310", "BM"); // Bermuda
|
||||||
|
MID_TO_COUNTRY.put("311", "BS"); // Bahamas
|
||||||
|
MID_TO_COUNTRY.put("312", "BZ"); // Belize
|
||||||
|
MID_TO_COUNTRY.put("314", "BB"); // Barbados
|
||||||
|
MID_TO_COUNTRY.put("316", "CA"); // Canada
|
||||||
|
MID_TO_COUNTRY.put("319", "KY"); // Cayman Islands
|
||||||
|
MID_TO_COUNTRY.put("321", "CR"); // Costa Rica
|
||||||
|
MID_TO_COUNTRY.put("323", "CU"); // Cuba
|
||||||
|
MID_TO_COUNTRY.put("325", "DM"); // Dominica
|
||||||
|
MID_TO_COUNTRY.put("327", "DO"); // Dominican Republic
|
||||||
|
MID_TO_COUNTRY.put("329", "GP"); // Guadeloupe
|
||||||
|
MID_TO_COUNTRY.put("330", "GD"); // Grenada
|
||||||
|
MID_TO_COUNTRY.put("331", "GL"); // Greenland
|
||||||
|
MID_TO_COUNTRY.put("332", "GT"); // Guatemala
|
||||||
|
MID_TO_COUNTRY.put("334", "HN"); // Honduras
|
||||||
|
MID_TO_COUNTRY.put("336", "HT"); // Haiti
|
||||||
|
MID_TO_COUNTRY.put("338", "US"); // USA
|
||||||
|
MID_TO_COUNTRY.put("339", "JM"); // Jamaica
|
||||||
|
MID_TO_COUNTRY.put("341", "KN"); // Saint Kitts and Nevis
|
||||||
|
MID_TO_COUNTRY.put("343", "LC"); // Saint Lucia
|
||||||
|
MID_TO_COUNTRY.put("345", "MX"); // Mexico
|
||||||
|
MID_TO_COUNTRY.put("347", "MQ"); // Martinique
|
||||||
|
MID_TO_COUNTRY.put("348", "MS"); // Montserrat
|
||||||
|
MID_TO_COUNTRY.put("350", "NI"); // Nicaragua
|
||||||
|
MID_TO_COUNTRY.put("351", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("352", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("353", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("354", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("355", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("356", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("357", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("358", "PR"); // Puerto Rico
|
||||||
|
MID_TO_COUNTRY.put("359", "SV"); // El Salvador
|
||||||
|
MID_TO_COUNTRY.put("361", "PM"); // Saint Pierre and Miquelon
|
||||||
|
MID_TO_COUNTRY.put("362", "TT"); // Trinidad and Tobago
|
||||||
|
MID_TO_COUNTRY.put("364", "TC"); // Turks and Caicos Islands
|
||||||
|
MID_TO_COUNTRY.put("366", "US"); // USA
|
||||||
|
MID_TO_COUNTRY.put("367", "US"); // USA
|
||||||
|
MID_TO_COUNTRY.put("368", "US"); // USA
|
||||||
|
MID_TO_COUNTRY.put("369", "US"); // USA
|
||||||
|
MID_TO_COUNTRY.put("370", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("371", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("372", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("373", "PA"); // Panama
|
||||||
|
MID_TO_COUNTRY.put("375", "VC"); // Saint Vincent and the Grenadines
|
||||||
|
MID_TO_COUNTRY.put("376", "VC"); // Saint Vincent and the Grenadines
|
||||||
|
MID_TO_COUNTRY.put("377", "VC"); // Saint Vincent and the Grenadines
|
||||||
|
MID_TO_COUNTRY.put("378", "VG"); // British Virgin Islands
|
||||||
|
MID_TO_COUNTRY.put("379", "VI"); // U.S. Virgin Islands
|
||||||
|
|
||||||
|
// Asia & Middle East
|
||||||
|
MID_TO_COUNTRY.put("401", "AF"); // Afghanistan
|
||||||
|
MID_TO_COUNTRY.put("403", "SA"); // Saudi Arabia
|
||||||
|
MID_TO_COUNTRY.put("405", "BD"); // Bangladesh
|
||||||
|
MID_TO_COUNTRY.put("408", "BH"); // Bahrain
|
||||||
|
MID_TO_COUNTRY.put("410", "BT"); // Bhutan
|
||||||
|
MID_TO_COUNTRY.put("412", "CN"); // China
|
||||||
|
MID_TO_COUNTRY.put("413", "CN"); // China
|
||||||
|
MID_TO_COUNTRY.put("414", "CN"); // China
|
||||||
|
MID_TO_COUNTRY.put("416", "TW"); // Taiwan
|
||||||
|
MID_TO_COUNTRY.put("417", "LK"); // Sri Lanka
|
||||||
|
MID_TO_COUNTRY.put("419", "IN"); // India
|
||||||
|
MID_TO_COUNTRY.put("422", "IR"); // Iran
|
||||||
|
MID_TO_COUNTRY.put("423", "AZ"); // Azerbaijan
|
||||||
|
MID_TO_COUNTRY.put("425", "IQ"); // Iraq
|
||||||
|
MID_TO_COUNTRY.put("428", "IL"); // Israel
|
||||||
|
MID_TO_COUNTRY.put("431", "JP"); // Japan
|
||||||
|
MID_TO_COUNTRY.put("432", "JP"); // Japan
|
||||||
|
MID_TO_COUNTRY.put("434", "TM"); // Turkmenistan
|
||||||
|
MID_TO_COUNTRY.put("436", "KZ"); // Kazakhstan
|
||||||
|
MID_TO_COUNTRY.put("437", "UZ"); // Uzbekistan
|
||||||
|
MID_TO_COUNTRY.put("438", "JO"); // Jordan
|
||||||
|
MID_TO_COUNTRY.put("440", "KR"); // South Korea
|
||||||
|
MID_TO_COUNTRY.put("441", "KR"); // South Korea
|
||||||
|
MID_TO_COUNTRY.put("443", "PS"); // Palestine
|
||||||
|
MID_TO_COUNTRY.put("445", "KP"); // North Korea
|
||||||
|
MID_TO_COUNTRY.put("447", "KW"); // Kuwait
|
||||||
|
MID_TO_COUNTRY.put("450", "LB"); // Lebanon
|
||||||
|
MID_TO_COUNTRY.put("451", "KG"); // Kyrgyzstan
|
||||||
|
MID_TO_COUNTRY.put("453", "MO"); // Macao
|
||||||
|
MID_TO_COUNTRY.put("455", "MV"); // Maldives
|
||||||
|
MID_TO_COUNTRY.put("457", "MN"); // Mongolia
|
||||||
|
MID_TO_COUNTRY.put("459", "NP"); // Nepal
|
||||||
|
MID_TO_COUNTRY.put("461", "OM"); // Oman
|
||||||
|
MID_TO_COUNTRY.put("463", "PK"); // Pakistan
|
||||||
|
MID_TO_COUNTRY.put("466", "QA"); // Qatar
|
||||||
|
MID_TO_COUNTRY.put("468", "SY"); // Syria
|
||||||
|
MID_TO_COUNTRY.put("470", "AE"); // United Arab Emirates
|
||||||
|
MID_TO_COUNTRY.put("471", "AE"); // United Arab Emirates
|
||||||
|
MID_TO_COUNTRY.put("472", "TJ"); // Tajikistan
|
||||||
|
MID_TO_COUNTRY.put("473", "YE"); // Yemen
|
||||||
|
MID_TO_COUNTRY.put("475", "YE"); // Yemen
|
||||||
|
MID_TO_COUNTRY.put("477", "HK"); // Hong Kong
|
||||||
|
MID_TO_COUNTRY.put("478", "BA"); // Bosnia and Herzegovina (legacy routing usage)
|
||||||
|
|
||||||
|
// Oceania
|
||||||
|
MID_TO_COUNTRY.put("501", "AQ"); // Antarctica
|
||||||
|
MID_TO_COUNTRY.put("503", "AU"); // Australia
|
||||||
|
MID_TO_COUNTRY.put("506", "MM"); // Myanmar
|
||||||
|
MID_TO_COUNTRY.put("508", "BN"); // Brunei
|
||||||
|
MID_TO_COUNTRY.put("510", "FM"); // Micronesia
|
||||||
|
MID_TO_COUNTRY.put("511", "PW"); // Palau
|
||||||
|
MID_TO_COUNTRY.put("512", "NZ"); // New Zealand
|
||||||
|
MID_TO_COUNTRY.put("514", "KH"); // Cambodia
|
||||||
|
MID_TO_COUNTRY.put("515", "KH"); // Cambodia
|
||||||
|
MID_TO_COUNTRY.put("516", "CX"); // Christmas Island
|
||||||
|
MID_TO_COUNTRY.put("518", "CK"); // Cook Islands
|
||||||
|
MID_TO_COUNTRY.put("520", "FJ"); // Fiji
|
||||||
|
MID_TO_COUNTRY.put("523", "CC"); // Cocos (Keeling) Islands
|
||||||
|
MID_TO_COUNTRY.put("525", "ID"); // Indonesia
|
||||||
|
MID_TO_COUNTRY.put("529", "KI"); // Kiribati
|
||||||
|
MID_TO_COUNTRY.put("531", "LA"); // Laos
|
||||||
|
MID_TO_COUNTRY.put("533", "MY"); // Malaysia
|
||||||
|
MID_TO_COUNTRY.put("536", "MP"); // Northern Mariana Islands
|
||||||
|
MID_TO_COUNTRY.put("538", "MH"); // Marshall Islands
|
||||||
|
MID_TO_COUNTRY.put("540", "NC"); // New Caledonia
|
||||||
|
MID_TO_COUNTRY.put("542", "NU"); // Niue
|
||||||
|
MID_TO_COUNTRY.put("544", "NR"); // Nauru
|
||||||
|
MID_TO_COUNTRY.put("546", "PF"); // French Polynesia
|
||||||
|
MID_TO_COUNTRY.put("548", "PH"); // Philippines
|
||||||
|
MID_TO_COUNTRY.put("553", "PG"); // Papua New Guinea
|
||||||
|
MID_TO_COUNTRY.put("555", "PN"); // Pitcairn Islands
|
||||||
|
MID_TO_COUNTRY.put("557", "SB"); // Solomon Islands
|
||||||
|
MID_TO_COUNTRY.put("559", "AS"); // American Samoa
|
||||||
|
MID_TO_COUNTRY.put("561", "WS"); // Samoa
|
||||||
|
MID_TO_COUNTRY.put("563", "SG"); // Singapore
|
||||||
|
MID_TO_COUNTRY.put("564", "SG"); // Singapore
|
||||||
|
MID_TO_COUNTRY.put("565", "SG"); // Singapore
|
||||||
|
MID_TO_COUNTRY.put("566", "SG"); // Singapore
|
||||||
|
MID_TO_COUNTRY.put("567", "TH"); // Thailand
|
||||||
|
MID_TO_COUNTRY.put("570", "TO"); // Tonga
|
||||||
|
MID_TO_COUNTRY.put("572", "TV"); // Tuvalu
|
||||||
|
MID_TO_COUNTRY.put("574", "VN"); // Vietnam
|
||||||
|
MID_TO_COUNTRY.put("576", "VU"); // Vanuatu
|
||||||
|
MID_TO_COUNTRY.put("578", "WF"); // Wallis and Futuna
|
||||||
|
|
||||||
|
// Africa
|
||||||
|
MID_TO_COUNTRY.put("601", "ZA"); // South Africa
|
||||||
|
MID_TO_COUNTRY.put("603", "AO"); // Angola
|
||||||
|
MID_TO_COUNTRY.put("605", "DZ"); // Algeria
|
||||||
|
MID_TO_COUNTRY.put("609", "BI"); // Burundi
|
||||||
|
MID_TO_COUNTRY.put("610", "BJ"); // Benin
|
||||||
|
MID_TO_COUNTRY.put("611", "BW"); // Botswana
|
||||||
|
MID_TO_COUNTRY.put("612", "CF"); // Central African Republic
|
||||||
|
MID_TO_COUNTRY.put("613", "CM"); // Cameroon
|
||||||
|
MID_TO_COUNTRY.put("615", "CG"); // Congo (Republic)
|
||||||
|
MID_TO_COUNTRY.put("616", "KM"); // Comoros
|
||||||
|
MID_TO_COUNTRY.put("617", "CV"); // Cabo Verde
|
||||||
|
MID_TO_COUNTRY.put("619", "CI"); // Côte d’Ivoire
|
||||||
|
MID_TO_COUNTRY.put("621", "DJ"); // Djibouti
|
||||||
|
MID_TO_COUNTRY.put("622", "EG"); // Egypt
|
||||||
|
MID_TO_COUNTRY.put("624", "ET"); // Ethiopia
|
||||||
|
MID_TO_COUNTRY.put("625", "ER"); // Eritrea
|
||||||
|
MID_TO_COUNTRY.put("626", "GA"); // Gabon
|
||||||
|
MID_TO_COUNTRY.put("627", "GH"); // Ghana
|
||||||
|
MID_TO_COUNTRY.put("629", "GM"); // Gambia
|
||||||
|
MID_TO_COUNTRY.put("630", "GW"); // Guinea-Bissau
|
||||||
|
MID_TO_COUNTRY.put("631", "GQ"); // Equatorial Guinea
|
||||||
|
MID_TO_COUNTRY.put("632", "GN"); // Guinea
|
||||||
|
MID_TO_COUNTRY.put("633", "BF"); // Burkina Faso
|
||||||
|
MID_TO_COUNTRY.put("634", "KE"); // Kenya
|
||||||
|
MID_TO_COUNTRY.put("636", "LR"); // Liberia
|
||||||
|
MID_TO_COUNTRY.put("637", "LR"); // Liberia
|
||||||
|
MID_TO_COUNTRY.put("642", "LY"); // Libya
|
||||||
|
MID_TO_COUNTRY.put("644", "LS"); // Lesotho
|
||||||
|
MID_TO_COUNTRY.put("645", "MU"); // Mauritius
|
||||||
|
MID_TO_COUNTRY.put("647", "MG"); // Madagascar
|
||||||
|
MID_TO_COUNTRY.put("649", "ML"); // Mali
|
||||||
|
MID_TO_COUNTRY.put("650", "MZ"); // Mozambique
|
||||||
|
MID_TO_COUNTRY.put("654", "MR"); // Mauritania
|
||||||
|
MID_TO_COUNTRY.put("655", "MW"); // Malawi
|
||||||
|
MID_TO_COUNTRY.put("656", "NE"); // Niger
|
||||||
|
MID_TO_COUNTRY.put("657", "NG"); // Nigeria
|
||||||
|
MID_TO_COUNTRY.put("659", "NA"); // Namibia
|
||||||
|
MID_TO_COUNTRY.put("660", "RE"); // Reunion (FR)
|
||||||
|
MID_TO_COUNTRY.put("661", "RW"); // Rwanda
|
||||||
|
MID_TO_COUNTRY.put("662", "SD"); // Sudan
|
||||||
|
MID_TO_COUNTRY.put("663", "SN"); // Senegal
|
||||||
|
MID_TO_COUNTRY.put("664", "SC"); // Seychelles
|
||||||
|
MID_TO_COUNTRY.put("665", "SH"); // Saint Helena
|
||||||
|
MID_TO_COUNTRY.put("666", "SO"); // Somalia
|
||||||
|
MID_TO_COUNTRY.put("667", "SL"); // Sierra Leone
|
||||||
|
MID_TO_COUNTRY.put("668", "ST"); // Sao Tome and Principe
|
||||||
|
MID_TO_COUNTRY.put("669", "SZ"); // Eswatini
|
||||||
|
MID_TO_COUNTRY.put("670", "TD"); // Chad
|
||||||
|
MID_TO_COUNTRY.put("671", "TG"); // Togo
|
||||||
|
MID_TO_COUNTRY.put("672", "TN"); // Tunisia
|
||||||
|
MID_TO_COUNTRY.put("674", "TZ"); // Tanzania
|
||||||
|
MID_TO_COUNTRY.put("675", "UG"); // Uganda
|
||||||
|
MID_TO_COUNTRY.put("676", "CD"); // DR Congo
|
||||||
|
MID_TO_COUNTRY.put("677", "TZ"); // Tanzania (alt)
|
||||||
|
MID_TO_COUNTRY.put("678", "ZM"); // Zambia
|
||||||
|
MID_TO_COUNTRY.put("679", "ZW"); // Zimbabwe
|
||||||
|
|
||||||
|
// South America
|
||||||
|
MID_TO_COUNTRY.put("701", "AR"); // Argentina
|
||||||
|
MID_TO_COUNTRY.put("710", "BR"); // Brazil
|
||||||
|
MID_TO_COUNTRY.put("720", "BO"); // Bolivia
|
||||||
|
MID_TO_COUNTRY.put("725", "CL"); // Chile
|
||||||
|
MID_TO_COUNTRY.put("730", "CO"); // Colombia
|
||||||
|
MID_TO_COUNTRY.put("735", "EC"); // Ecuador
|
||||||
|
MID_TO_COUNTRY.put("740", "FK"); // Falkland Islands
|
||||||
|
MID_TO_COUNTRY.put("745", "GF"); // French Guiana
|
||||||
|
MID_TO_COUNTRY.put("750", "GY"); // Guyana
|
||||||
|
MID_TO_COUNTRY.put("755", "PY"); // Paraguay
|
||||||
|
MID_TO_COUNTRY.put("760", "PE"); // Peru
|
||||||
|
MID_TO_COUNTRY.put("765", "SR"); // Suriname
|
||||||
|
MID_TO_COUNTRY.put("770", "UY"); // Uruguay
|
||||||
|
MID_TO_COUNTRY.put("775", "VE"); // Venezuela
|
||||||
|
}
|
||||||
|
|
||||||
|
private MIDToCountry() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -19,6 +19,15 @@ public class SettingsManager {
|
|||||||
private static final String KEY_ANDROID_NMEA_ENABLED = "android_nmea_enabled";
|
private static final String KEY_ANDROID_NMEA_ENABLED = "android_nmea_enabled";
|
||||||
private static final String KEY_UDP_NMEA_ENABLED = "udp_nmea_enabled";
|
private static final String KEY_UDP_NMEA_ENABLED = "udp_nmea_enabled";
|
||||||
private static final String KEY_DATA_MODE = "data_mode";
|
private static final String KEY_DATA_MODE = "data_mode";
|
||||||
|
private static final String KEY_DATA_STALE_WARNING_MINUTES = "data_stale_warning_minutes";
|
||||||
|
private static final String KEY_DATA_STALE_REMOVE_MINUTES = "data_stale_remove_minutes";
|
||||||
|
private static final String KEY_PATH_TRACKING_ENABLED = "path_tracking_enabled";
|
||||||
|
private static final String KEY_PATH_COLOR = "path_color";
|
||||||
|
private static final String KEY_PREDICTION_COLOR = "prediction_color";
|
||||||
|
private static final String KEY_PATH_WIDTH = "path_width";
|
||||||
|
private static final String KEY_PREDICTION_WIDTH = "prediction_width";
|
||||||
|
private static final String KEY_VIBRATION_ENABLED = "vibration_enabled";
|
||||||
|
private static final String KEY_SOUND_ENABLED = "sound_enabled";
|
||||||
|
|
||||||
// Значения по умолчанию
|
// Значения по умолчанию
|
||||||
private static final int DEFAULT_UDP_PORT = 10110;
|
private static final int DEFAULT_UDP_PORT = 10110;
|
||||||
@@ -26,6 +35,15 @@ public class SettingsManager {
|
|||||||
private static final boolean DEFAULT_ANDROID_NMEA_ENABLED = true;
|
private static final boolean DEFAULT_ANDROID_NMEA_ENABLED = true;
|
||||||
private static final boolean DEFAULT_UDP_NMEA_ENABLED = true;
|
private static final boolean DEFAULT_UDP_NMEA_ENABLED = true;
|
||||||
private static final String DEFAULT_DATA_MODE = "hybrid";
|
private static final String DEFAULT_DATA_MODE = "hybrid";
|
||||||
|
private static final int DEFAULT_DATA_STALE_WARNING_MINUTES = 5; // Показывать losingtarget.xml
|
||||||
|
private static final int DEFAULT_DATA_STALE_REMOVE_MINUTES = 7; // Удалять из списка
|
||||||
|
private static final boolean DEFAULT_PATH_TRACKING_ENABLED = true;
|
||||||
|
private static final int DEFAULT_PATH_COLOR = 0xFF00FFFF; // Голубой
|
||||||
|
private static final int DEFAULT_PREDICTION_COLOR = 0xFFFFFF00; // Желтый
|
||||||
|
private static final float DEFAULT_PATH_WIDTH = 3.0f;
|
||||||
|
private static final float DEFAULT_PREDICTION_WIDTH = 2.0f;
|
||||||
|
private static final boolean DEFAULT_VIBRATION_ENABLED = true;
|
||||||
|
private static final boolean DEFAULT_SOUND_ENABLED = true;
|
||||||
|
|
||||||
// Режимы работы с данными
|
// Режимы работы с данными
|
||||||
public static final String DATA_MODE_HYBRID = "hybrid";
|
public static final String DATA_MODE_HYBRID = "hybrid";
|
||||||
@@ -163,6 +181,10 @@ public class SettingsManager {
|
|||||||
.putBoolean(KEY_ANDROID_NMEA_ENABLED, DEFAULT_ANDROID_NMEA_ENABLED)
|
.putBoolean(KEY_ANDROID_NMEA_ENABLED, DEFAULT_ANDROID_NMEA_ENABLED)
|
||||||
.putBoolean(KEY_UDP_NMEA_ENABLED, DEFAULT_UDP_NMEA_ENABLED)
|
.putBoolean(KEY_UDP_NMEA_ENABLED, DEFAULT_UDP_NMEA_ENABLED)
|
||||||
.putString(KEY_DATA_MODE, DEFAULT_DATA_MODE)
|
.putString(KEY_DATA_MODE, DEFAULT_DATA_MODE)
|
||||||
|
.putInt(KEY_DATA_STALE_WARNING_MINUTES, DEFAULT_DATA_STALE_WARNING_MINUTES)
|
||||||
|
.putInt(KEY_DATA_STALE_REMOVE_MINUTES, DEFAULT_DATA_STALE_REMOVE_MINUTES)
|
||||||
|
.putBoolean(KEY_VIBRATION_ENABLED, DEFAULT_VIBRATION_ENABLED)
|
||||||
|
.putBoolean(KEY_SOUND_ENABLED, DEFAULT_SOUND_ENABLED)
|
||||||
.apply();
|
.apply();
|
||||||
Log.i(TAG, "Настройки сброшены к значениям по умолчанию");
|
Log.i(TAG, "Настройки сброшены к значениям по умолчанию");
|
||||||
}
|
}
|
||||||
@@ -175,12 +197,15 @@ public class SettingsManager {
|
|||||||
"UDP: порт=%d, включен=%s\n" +
|
"UDP: порт=%d, включен=%s\n" +
|
||||||
"Android NMEA: %s\n" +
|
"Android NMEA: %s\n" +
|
||||||
"UDP NMEA: %s\n" +
|
"UDP NMEA: %s\n" +
|
||||||
"Режим данных: %s",
|
"Режим данных: %s\n" +
|
||||||
|
"Уведомления: вибрация=%s, звук=%s",
|
||||||
getUDPPort(),
|
getUDPPort(),
|
||||||
isUDPEnabled() ? "да" : "нет",
|
isUDPEnabled() ? "да" : "нет",
|
||||||
isAndroidNMEAEnabled() ? "включен" : "выключен",
|
isAndroidNMEAEnabled() ? "включен" : "выключен",
|
||||||
isUDPNMEAEnabled() ? "включен" : "выключен",
|
isUDPNMEAEnabled() ? "включен" : "выключен",
|
||||||
getDataMode()
|
getDataMode(),
|
||||||
|
isVibrationEnabled() ? "включена" : "выключена",
|
||||||
|
isSoundEnabled() ? "включен" : "выключен"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,4 +224,155 @@ public class SettingsManager {
|
|||||||
isUDPNMEAEnabled() != currentUDPNMEA ||
|
isUDPNMEAEnabled() != currentUDPNMEA ||
|
||||||
!getDataMode().equals(currentDataMode);
|
!getDataMode().equals(currentDataMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает время предупреждения об устаревших данных (в минутах)
|
||||||
|
*/
|
||||||
|
public int getDataStaleWarningMinutes() {
|
||||||
|
return prefs.getInt(KEY_DATA_STALE_WARNING_MINUTES, DEFAULT_DATA_STALE_WARNING_MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает время предупреждения об устаревших данных (в минутах)
|
||||||
|
*/
|
||||||
|
public void setDataStaleWarningMinutes(int minutes) {
|
||||||
|
if (minutes < 1 || minutes > 60) {
|
||||||
|
Log.w(TAG, "Некорректное время предупреждения: " + minutes + ", используем значение по умолчанию");
|
||||||
|
minutes = DEFAULT_DATA_STALE_WARNING_MINUTES;
|
||||||
|
}
|
||||||
|
prefs.edit().putInt(KEY_DATA_STALE_WARNING_MINUTES, minutes).apply();
|
||||||
|
Log.i(TAG, "Время предупреждения об устаревших данных установлено: " + minutes + " минут");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает время удаления устаревших данных (в минутах)
|
||||||
|
*/
|
||||||
|
public int getDataStaleRemoveMinutes() {
|
||||||
|
return prefs.getInt(KEY_DATA_STALE_REMOVE_MINUTES, DEFAULT_DATA_STALE_REMOVE_MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает время удаления устаревших данных (в минутах)
|
||||||
|
*/
|
||||||
|
public void setDataStaleRemoveMinutes(int minutes) {
|
||||||
|
if (minutes < 1 || minutes > 60) {
|
||||||
|
Log.w(TAG, "Некорректное время удаления: " + minutes + ", используем значение по умолчанию");
|
||||||
|
minutes = DEFAULT_DATA_STALE_REMOVE_MINUTES;
|
||||||
|
}
|
||||||
|
prefs.edit().putInt(KEY_DATA_STALE_REMOVE_MINUTES, minutes).apply();
|
||||||
|
Log.i(TAG, "Время удаления устаревших данных установлено: " + minutes + " минут");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включено ли отслеживание путей
|
||||||
|
*/
|
||||||
|
public boolean isPathTrackingEnabled() {
|
||||||
|
return prefs.getBoolean(KEY_PATH_TRACKING_ENABLED, DEFAULT_PATH_TRACKING_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает отслеживание путей
|
||||||
|
*/
|
||||||
|
public void setPathTrackingEnabled(boolean enabled) {
|
||||||
|
prefs.edit().putBoolean(KEY_PATH_TRACKING_ENABLED, enabled).apply();
|
||||||
|
Log.i(TAG, "Отслеживание путей: " + (enabled ? "включено" : "выключено"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает цвет пройденного пути
|
||||||
|
*/
|
||||||
|
public int getPathColor() {
|
||||||
|
return prefs.getInt(KEY_PATH_COLOR, DEFAULT_PATH_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает цвет пройденного пути
|
||||||
|
*/
|
||||||
|
public void setPathColor(int color) {
|
||||||
|
prefs.edit().putInt(KEY_PATH_COLOR, color).apply();
|
||||||
|
Log.i(TAG, "Цвет пройденного пути установлен: " + String.format("#%08X", color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает цвет прогнозируемого пути
|
||||||
|
*/
|
||||||
|
public int getPredictionColor() {
|
||||||
|
return prefs.getInt(KEY_PREDICTION_COLOR, DEFAULT_PREDICTION_COLOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает цвет прогнозируемого пути
|
||||||
|
*/
|
||||||
|
public void setPredictionColor(int color) {
|
||||||
|
prefs.edit().putInt(KEY_PREDICTION_COLOR, color).apply();
|
||||||
|
Log.i(TAG, "Цвет прогнозируемого пути установлен: " + String.format("#%08X", color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает ширину линии пройденного пути
|
||||||
|
*/
|
||||||
|
public float getPathWidth() {
|
||||||
|
return prefs.getFloat(KEY_PATH_WIDTH, DEFAULT_PATH_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает ширину линии пройденного пути
|
||||||
|
*/
|
||||||
|
public void setPathWidth(float width) {
|
||||||
|
if (width < 1.0f || width > 10.0f) {
|
||||||
|
Log.w(TAG, "Некорректная ширина пути: " + width + ", используем значение по умолчанию");
|
||||||
|
width = DEFAULT_PATH_WIDTH;
|
||||||
|
}
|
||||||
|
prefs.edit().putFloat(KEY_PATH_WIDTH, width).apply();
|
||||||
|
Log.i(TAG, "Ширина пройденного пути установлена: " + width);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает ширину линии прогнозируемого пути
|
||||||
|
*/
|
||||||
|
public float getPredictionWidth() {
|
||||||
|
return prefs.getFloat(KEY_PREDICTION_WIDTH, DEFAULT_PREDICTION_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Устанавливает ширину линии прогнозируемого пути
|
||||||
|
*/
|
||||||
|
public void setPredictionWidth(float width) {
|
||||||
|
if (width < 1.0f || width > 10.0f) {
|
||||||
|
Log.w(TAG, "Некорректная ширина прогноза: " + width + ", используем значение по умолчанию");
|
||||||
|
width = DEFAULT_PREDICTION_WIDTH;
|
||||||
|
}
|
||||||
|
prefs.edit().putFloat(KEY_PREDICTION_WIDTH, width).apply();
|
||||||
|
Log.i(TAG, "Ширина прогнозируемого пути установлена: " + width);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включена ли вибрация при обнаружении новых AIS целей
|
||||||
|
*/
|
||||||
|
public boolean isVibrationEnabled() {
|
||||||
|
return prefs.getBoolean(KEY_VIBRATION_ENABLED, DEFAULT_VIBRATION_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает вибрацию при обнаружении новых AIS целей
|
||||||
|
*/
|
||||||
|
public void setVibrationEnabled(boolean enabled) {
|
||||||
|
prefs.edit().putBoolean(KEY_VIBRATION_ENABLED, enabled).apply();
|
||||||
|
Log.i(TAG, "Вибрация при обнаружении новых AIS целей: " + (enabled ? "включена" : "выключена"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверяет, включен ли звук при обнаружении новых AIS целей
|
||||||
|
*/
|
||||||
|
public boolean isSoundEnabled() {
|
||||||
|
return prefs.getBoolean(KEY_SOUND_ENABLED, DEFAULT_SOUND_ENABLED);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Включает/выключает звук при обнаружении новых AIS целей
|
||||||
|
*/
|
||||||
|
public void setSoundEnabled(boolean enabled) {
|
||||||
|
prefs.edit().putBoolean(KEY_SOUND_ENABLED, enabled).apply();
|
||||||
|
Log.i(TAG, "Звук при обнаружении новых AIS целей: " + (enabled ? "включен" : "выключен"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,16 @@
|
|||||||
android:height="162.6dp"
|
android:height="162.6dp"
|
||||||
android:viewportWidth="91.38"
|
android:viewportWidth="91.38"
|
||||||
android:viewportHeight="162.6">
|
android:viewportHeight="162.6">
|
||||||
|
<!-- Внешняя обводка для контраста -->
|
||||||
<path
|
<path
|
||||||
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
android:pathData="M45.69,16.63l-39.75,141.47l79.5,0l-39.75,-141.47z"
|
||||||
android:strokeWidth="9"
|
android:strokeWidth="12"
|
||||||
android:fillColor="#00000000"
|
android:fillColor="#000000"
|
||||||
android:strokeColor="#000"/>
|
android:strokeColor="#000000"/>
|
||||||
|
<!-- Основная форма с внутренней обводкой -->
|
||||||
|
<path
|
||||||
|
android:pathData="M45.69,18.63l-37.75,139.47l75.5,0l-37.75,-139.47z"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:fillColor="#B5B8B1"
|
||||||
|
android:strokeColor="#FFFFFF"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="88.5dp"
|
||||||
|
android:height="152.73dp"
|
||||||
|
android:viewportWidth="88.5"
|
||||||
|
android:viewportHeight="152.73">
|
||||||
|
<!-- Внешняя обводка для контраста -->
|
||||||
|
<path
|
||||||
|
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||||
|
android:strokeWidth="9"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:strokeColor="#000000"/>
|
||||||
|
<!-- Основная форма с внутренней обводкой -->
|
||||||
|
<path
|
||||||
|
android:pathData="M44.25,6.77l-39.75,44.69l0,96.77l79.5,0l0,-96.77l-39.75,-44.69z"
|
||||||
|
android:strokeWidth="3"
|
||||||
|
android:fillColor="#B5B8B1"
|
||||||
|
android:strokeColor="#FFFFFF"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_ais_targets"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_empty_state"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:text="Нет AIS целей\nВсе корабли уплыли"
|
||||||
|
android:textAlignment="center"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
@@ -54,6 +54,35 @@
|
|||||||
android:minWidth="120dp"
|
android:minWidth="120dp"
|
||||||
android:background="@android:color/white" />
|
android:background="@android:color/white" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_ais_targets"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Цели AIS"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:minWidth="120dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
<!-- Строки возраста последних сообщений GPS ($) и AIS (!) -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_gps_age"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="GPS: --"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:layout_marginTop="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_ais_age"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="AIS: --"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:layout_marginTop="4dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Компас -->
|
<!-- Компас -->
|
||||||
|
|||||||
@@ -209,6 +209,152 @@
|
|||||||
|
|
||||||
</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_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⏰ Устаревание данных AIS"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Настройте время, через которое данные о судах считаются устаревшими:"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="Время предупреждения (минуты)"
|
||||||
|
app:helperText="Судна старше этого времени будут помечены как устаревшие">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_stale_warning_minutes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="5" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:hint="Время удаления (минуты)"
|
||||||
|
app:helperText="Судна старше этого времени будут удалены с карты">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/et_stale_remove_minutes"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:inputType="number"
|
||||||
|
android:text="7" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="💡 Совет: Устаревшие суда отображаются с иконкой losingtarget.xml"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginTop="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
||||||
|
<!-- Настройки уведомлений -->
|
||||||
|
<com.google.android.material.card.MaterialCardView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
app:cardCornerRadius="8dp"
|
||||||
|
app:cardElevation="4dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="🔔 Уведомления о новых целях AIS"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:layout_marginBottom="12dp" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Настройте уведомления при обнаружении новых судов:"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/darker_gray"
|
||||||
|
android:layout_marginBottom="16dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_vibration_enabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Вибрация"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:checked="true"
|
||||||
|
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="16dp"
|
||||||
|
android:layout_marginStart="16dp" />
|
||||||
|
|
||||||
|
<com.google.android.material.switchmaterial.SwitchMaterial
|
||||||
|
android:id="@+id/switch_sound_enabled"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="Звуковое уведомление"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:checked="true"
|
||||||
|
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>
|
||||||
|
|
||||||
<!-- Кнопки -->
|
<!-- Кнопки -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|||||||
@@ -46,6 +46,17 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Время назад -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_ais_time_ago"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="⏱️ Время назад: --"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:padding="8dp" />
|
||||||
<!-- MMSI -->
|
<!-- MMSI -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/bottom_sheet_ais_mmsi"
|
android:id="@+id/bottom_sheet_ais_mmsi"
|
||||||
@@ -58,17 +69,6 @@
|
|||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
android:padding="8dp" />
|
android:padding="8dp" />
|
||||||
|
|
||||||
<!-- Название судна -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bottom_sheet_ais_name"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="📛 Название: --"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="@android:color/black"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:padding="8dp" />
|
|
||||||
|
|
||||||
<!-- Позывной -->
|
<!-- Позывной -->
|
||||||
<TextView
|
<TextView
|
||||||
@@ -119,17 +119,52 @@
|
|||||||
android:padding="8dp" />
|
android:padding="8dp" />
|
||||||
|
|
||||||
<!-- Курс -->
|
<!-- Курс -->
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/bottom_sheet_ais_course"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="🧭 Курс: --°"
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_ais_course"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="🧭 COG: --°"
|
||||||
android:textSize="14sp"
|
android:textSize="14sp"
|
||||||
android:textColor="@android:color/black"
|
android:textColor="@android:color/black"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginBottom="8dp"
|
||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
android:padding="8dp" />
|
android:padding="8dp" />
|
||||||
|
|
||||||
|
<!-- Направление -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_ais_heading"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="🧭 HDG: --°"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:padding="8dp" />
|
||||||
|
|
||||||
|
<!-- Скорость поворота -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_ais_rot"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="🔄 ROT: --°/мин"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="@android:color/black"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:padding="8dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Скорость -->
|
<!-- Скорость -->
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/bottom_sheet_ais_speed"
|
android:id="@+id/bottom_sheet_ais_speed"
|
||||||
@@ -238,17 +273,7 @@
|
|||||||
android:background="@android:color/transparent"
|
android:background="@android:color/transparent"
|
||||||
android:padding="8dp" />
|
android:padding="8dp" />
|
||||||
|
|
||||||
<!-- Время назад -->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bottom_sheet_ais_time_ago"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="⏱️ Время назад: --"
|
|
||||||
android:textSize="14sp"
|
|
||||||
android:textColor="@android:color/black"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:padding="8dp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:text="Vessel" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_mmsi"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:text="MMSI" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_coords"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:text="0, 0" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_course_speed"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:text="COG 0 • 0 kn" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_last_update"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:text="Обновлено: --" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tv_time_ago"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:text="N сек назад" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_marine_traffic"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="MarineTraffic" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btn_center_on_map"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="На карте"
|
||||||
|
android:layout_marginStart="12dp" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
@@ -20,4 +20,10 @@
|
|||||||
android:icon="@android:drawable/ic_menu_delete"
|
android:icon="@android:drawable/ic_menu_delete"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_path_tracking"
|
||||||
|
android:title="Пути"
|
||||||
|
android:icon="@android:drawable/ic_menu_directions"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="_Слой_2" data-name="Слой_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 88.5 152.73">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: none;
|
||||||
|
stroke: #000;
|
||||||
|
stroke-miterlimit: 10;
|
||||||
|
stroke-width: 9px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<g id="_Слой_7" data-name="Слой_7">
|
||||||
|
<polygon class="cls-1" points="44.25 6.77 4.5 51.46 4.5 148.23 84 148.23 84 51.46 44.25 6.77"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 472 B |
Reference in New Issue
Block a user