commit 655e452cdd408e76884f21e77a6ce74187f71ba3 Author: Andrey Limasov Date: Wed Jun 17 17:25:57 2026 +0300 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..b03c33b --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +SafeRoute \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..757f88e --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/Presentation.pdf b/Presentation.pdf new file mode 100644 index 0000000..4813e54 Binary files /dev/null and b/Presentation.pdf differ diff --git a/Presentation.pptx b/Presentation.pptx new file mode 100644 index 0000000..3809e69 Binary files /dev/null and b/Presentation.pptx differ diff --git a/app.apk b/app.apk new file mode 100644 index 0000000..0a7da6e Binary files /dev/null and b/app.apk differ diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d80d567 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,55 @@ +plugins { + id("com.android.application") +} + +android { + namespace = "com.example.saferoute" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.saferoute" + minSdk = 26 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("com.google.android.material:material:1.13.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.1") + implementation ("androidx.recyclerview:recyclerview:1.2.1") + + implementation("com.google.android.gms:play-services-location:21.3.0") + implementation("com.google.android.gms:play-services-maps:20.0.0") + testImplementation("junit:junit:4.13.2") + implementation("com.yandex.android:maps.mobile:4.4.0-full") + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + androidTestImplementation("androidx.test.ext:junit:1.3.0") + androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0") + + // Добавление зависимости Gson + implementation ("com.google.code.gson:gson:2.10.1") + + implementation ("org.osmdroid:osmdroid-android:6.1.14") + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..6e8f54e --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,24 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile + +-keep class com.yandex.mapkit.** { *; } +-dontwarn com.yandex.** \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/saferoute/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/example/saferoute/ExampleInstrumentedTest.java new file mode 100644 index 0000000..7569165 --- /dev/null +++ b/app/src/androidTest/java/com/example/saferoute/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.saferoute; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.saferoute", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..efc6b96 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..1ce0d70 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/example/saferoute/MainActivity.java b/app/src/main/java/com/example/saferoute/MainActivity.java new file mode 100644 index 0000000..a154d09 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/MainActivity.java @@ -0,0 +1,668 @@ +package com.example.saferoute; + +import android.Manifest; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.media.Ringtone; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Vibrator; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.FragmentActivity; + +import com.example.saferoute.contacts.Contact; +import com.example.saferoute.contacts.ContactStorage; +import com.example.saferoute.places.Place; +import com.example.saferoute.places.PlaceStorage; +import com.example.saferoute.places.Point; +import com.example.saferoute.places.Route; +import com.example.saferoute.places.RouteStorage; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; +import com.google.android.gms.location.LocationServices; + +import org.osmdroid.config.Configuration; +import org.osmdroid.tileprovider.tilesource.TileSourceFactory; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class MainActivity extends FragmentActivity { + + private static final String TAG = "MainActivity"; + private static final int LOCATION_PERMISSION_REQUEST_CODE = 1001; + + private FusedLocationProviderClient fusedLocationClient; + private LocationCallback locationCallback; + private MapView mapView; + + // Текущие координаты (используем единый источник) + private double currentLat = 0.0; + private double currentLon = 0.0; + + private LocationManager locationManager; + private LocationListener locationListener; + + private List places = new ArrayList<>(); + private List routes = new ArrayList<>(); + + private Place selectedPlace; + private Route selectedRoute; + + + private ContactStorage contactStorage; + private PlaceStorage placeStorage; + private RouteStorage routeStorage; + + + private Button btnGetRoute, btnSOS; + + // Навигация + private static final double MAX_DEVIATION_METERS = 30; + private List routePoints; + private int currentSegmentIndex = 0; + private boolean isNavigationActive = false; + + // Request permission launcher (регистрация в onCreate) + private ActivityResultLauncher requestPermissionLauncher; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Osmdroid конфиг — ставим до setContentView + Configuration.getInstance().load(this, getSharedPreferences("osmdroid", MODE_PRIVATE)); + Configuration.getInstance().setUserAgentValue(getPackageName()); + + setContentView(R.layout.activity_main); + + // ========== Инициализация UI и объектов ========== + mapView = findViewById(R.id.map); + btnGetRoute = findViewById(R.id.btnStartRoute); + btnSOS = findViewById(R.id.btnSOS); + + contactStorage = new ContactStorage(this); + placeStorage = new PlaceStorage(this); + places = placeStorage.loadPlaces(); + routeStorage = new RouteStorage(this); + routes = routeStorage.loadRoutesSync(); + + // Настройка карты + mapView.setTileSource(TileSourceFactory.MAPNIK); + mapView.setBuiltInZoomControls(true); + mapView.setMultiTouchControls(true); + + // Инициализация fusedLocationClient до любых вызовов локации! + fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); + + // Локационный колбэк для активного запроса + locationCallback = new LocationCallback() { + @Override + public void onLocationResult(@NonNull LocationResult locationResult) { + if (locationResult == null) return; + for (Location loc : locationResult.getLocations()) { + if (loc != null) { + currentLat = loc.getLatitude(); + currentLon = loc.getLongitude(); + Log.d(TAG, "Активное получение: " + currentLat + ", " + currentLon); + centerMapOnLocation(currentLat, currentLon); + stopActiveLocationRequests(); // получили — останавливаем + } + } + } + }; + + // ActivityResult API — регистрируем здесь (гарантировано после onAttach) + requestPermissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + isGranted -> { + if (isGranted) { + Log.d(TAG, "Разрешение получено через ActivityResultLauncher"); + getAccurateLocation(); + } else { + Toast.makeText(this, "Разрешение на локацию не предоставлено", Toast.LENGTH_SHORT).show(); + } + }); + + // Обработчики кнопок + btnGetRoute.setOnClickListener(v -> showDestinationDialog()); + btnSOS.setOnClickListener(v -> SOS(v)); + + // Не запрашиваем права дважды: используем единый поток + checkAndRequestLocationPermission(); + + // Центрируем карту на текущих координатах (если есть last location — будет установлен в getAccurateLocation) + // В любом случае устанавливаем минимальный центр, чтобы карта показалась сразу + mapView.getController().setZoom(15); + mapView.getController().setCenter(new GeoPoint(0.0, 0.0)); + } + + private void checkAndRequestLocationPermission() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + getAccurateLocation(); + } else { + requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION); + } + } + + // Центрирование карты + private void centerMapOnLocation(double latitude, double longitude) { + if (mapView == null) return; + try { + mapView.getController().setZoom(16.5); + mapView.getController().setCenter(new GeoPoint(latitude, longitude)); + mapView.invalidate(); + Log.d(TAG, "Карта центрирована: " + latitude + ", " + longitude); + } catch (Exception e) { + Log.e(TAG, "Ошибка центрирования карты", e); + } + } + + // Получаем last location или делаем активный запрос + private void getAccurateLocation() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + Log.w(TAG, "getAccurateLocation вызван без разрешения"); + return; + } + + fusedLocationClient.getLastLocation() + .addOnSuccessListener(this, location -> { + if (location != null) { + currentLat = location.getLatitude(); + currentLon = location.getLongitude(); + Log.d(TAG, "getLastLocation: " + currentLat + ", " + currentLon); + centerMapOnLocation(currentLat, currentLon); + } else { + Log.d(TAG, "getLastLocation вернул null, делаем активный запрос"); + startActiveLocationRequest(); + } + }) + .addOnFailureListener(this, e -> { + Log.e(TAG, "Ошибка getLastLocation: " + e.getMessage()); + startActiveLocationRequest(); + }); + } + + private void startActiveLocationRequest() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) return; + + LocationRequest lr = LocationRequest.create() + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) + .setNumUpdates(1) + .setInterval(2000); + + fusedLocationClient.requestLocationUpdates(lr, locationCallback, getMainLooper()); + Log.d(TAG, "Запрошено активное получение локации"); + } + + private void stopActiveLocationRequests() { + if (fusedLocationClient != null && locationCallback != null) { + fusedLocationClient.removeLocationUpdates(locationCallback); + Log.d(TAG, "Активный запрос локации остановлен"); + } + } + + // ========= Dialog и построение маршрута (ORS) ========= + private void showDestinationDialog() { + if ((places == null || places.isEmpty()) && routes == null || routes.isEmpty()) { + Toast.makeText(this, "Список мест пуст", Toast.LENGTH_SHORT).show(); + return; + } + + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Выберите точку назначения"); + + List list = new ArrayList<>(); + for (Place item : places) + list.add(item.getTitle()); + for (Route item : routes) + list.add(item.getTitle()); + + String[] result = list.toArray(new String[0]); + + builder.setSingleChoiceItems(result, -1, (dialog, which) -> { + if (which < places.size()) { + selectedPlace = places.get(which); + dialog.dismiss(); + buildRouteToPlace(); + } else { + selectedRoute = routes.get(which - places.size()); + dialog.dismiss(); + buildRoute(); + + } + Toast.makeText(this, "Выбрано: " + result[which], Toast.LENGTH_SHORT).show(); + + + }); + builder.setNegativeButton("Отмена", (dialog, which) -> dialog.dismiss()); + builder.show(); + + } + + + private void buildRouteToPlace() { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "Нет доступа к локации", Toast.LENGTH_SHORT).show(); + return; + } + + // Убедимся, что у нас есть актуальные координаты + final double startLon = currentLon; + final double startLat = currentLat; + final double endLon = selectedPlace.getLongitude(); + final double endLat = selectedPlace.getLatitude(); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.openrouteservice.org/") + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + ORSService service = retrofit.create(ORSService.class); + String apiKey = "eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjVhNDU5MGQxOTNjNzQxYTNhOTA4MDY5Nzg4ZWUxMDM1IiwiaCI6Im11cm11cjY0In0="; + +// Получаем координаты для запроса в формате "lat,lon" + String start = startLon + "," + startLat; + String end = endLon + "," + endLat; + + service.getFootRoute(apiKey, start, end).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null && !response.body().features.isEmpty()) { + List> coords = response.body().features.get(0).geometry.coordinates; + List route = new ArrayList<>(); + for (List p : coords) route.add(new GeoPoint(p.get(1), p.get(0))); + drawRouteOnMap(route); + startNavigation(route); + } else { + Log.e(TAG, "Ошибка маршрута. Код: " + response.code()); + Toast.makeText(MainActivity.this, "Ошибка получения маршрута", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e(TAG, "Network error: " + t.getMessage()); + Toast.makeText(MainActivity.this, "Ошибка сети: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + + + private void buildRoute() { + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.openrouteservice.org/") // НОВЫЙ URL + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + ORSService service = retrofit.create(ORSService.class); + + // Преобразуем List в формат для API [[lon, lat], [lon, lat], ...] + List coordsForApi = new ArrayList<>(); + for (Point p : selectedRoute.points) { + coordsForApi.add(new double[]{p.lon, p.lat}); + } + + ORSRequest requestBody = new ORSRequest(coordsForApi); + String apiKey = "eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjVhNDU5MGQxOTNjNzQxYTNhOTA4MDY5Nzg4ZWUxMDM1IiwiaCI6Im11cm11cjY0In0="; + + service.getMultiPointRoute(apiKey, requestBody).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + + + // Получаем «сырые» координаты пути от ORS [[lon, lat], [lon, lat], ...] + List> rawCoords = response.body().features.get(0).geometry.coordinates; + + // Конвертируем их в List + List routePoints = new ArrayList<>(); + for (List coord : rawCoords) { + // Важно: ORS возвращает [Lon, Lat], а GeoPoint создается как (Lat, Lon) + double lon = coord.get(0); + double lat = coord.get(1); + routePoints.add(new GeoPoint(lat, lon)); + } + + // Передаем готовый список в ваш метод отрисовки + drawRoute(routePoints); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("API", "Ошибка: " + t.getMessage()); + } + }); + } + + private void drawRoute(List points) { + if (points == null || points.isEmpty()) return; + + // Сохраняем, что это текущий показанный маршрут + List lastRoutePoints = new ArrayList<>(points); + + +// третья версия + // Удаляем старые Polyline-ы безопасно (итерация с конца) + for (int i = mapView.getOverlays().size() - 1; i >= 0; i--) { + org.osmdroid.views.overlay.Overlay ov = mapView.getOverlays().get(i); + if (ov instanceof org.osmdroid.views.overlay.Polyline) { + mapView.getOverlays().remove(i); + } + } + + org.osmdroid.views.overlay.Polyline polyline = new org.osmdroid.views.overlay.Polyline(); + polyline.setPoints(points); + polyline.setColor(0x880000FF); // Полупрозрачный синий + polyline.setWidth(10f); + + + mapView.getOverlays().add(polyline); + mapView.invalidate(); // Перерисовываем карту + + // Автоматически центрируем и масштабируем карту под маршрут + try { + mapView.zoomToBoundingBox(polyline.getBounds(), false); + } catch (Exception e) { + Log.w("DRAW_ROUTE", "Не удалось масштабировать bounding box: " + e.getMessage()); + } + } + + + // Безопасное удаление старых overlay-ов и отрисовка новой линии + private void drawRouteOnMap(List points) { + if (mapView == null || points == null || points.isEmpty()) return; + + // Удаляем старые Polyline-ы безопасно (итерируем с конца) + List overlays = mapView.getOverlays(); + for (int i = overlays.size() - 1; i >= 0; i--) { + org.osmdroid.views.overlay.Overlay ov = overlays.get(i); + if (ov instanceof org.osmdroid.views.overlay.Polyline) { + overlays.remove(i); + } + } + + org.osmdroid.views.overlay.Polyline polyline = new org.osmdroid.views.overlay.Polyline(); + polyline.setPoints(points); + polyline.setColor(0x880000FF); + polyline.setWidth(10f); + mapView.getOverlays().add(polyline); + mapView.invalidate(); + + try { + mapView.zoomToBoundingBox(polyline.getBounds(), false); + } catch (Exception e) { + Log.w(TAG, "Не удалось масштабировать bbox: " + e.getMessage()); + } + } + + // ========== Навигация и позиционирование ========== + + public void startNavigation(List points) { + if (points == null || points.isEmpty()) return; + this.routePoints = points; + currentSegmentIndex = 0; + isNavigationActive = true; + startLocationUpdatesForNavigation(); + } + + private void updateCurrentPosition(Location location) { + if (mapView == null || location == null) return; + + // Удаляем старые маркеры безопасно + List overlays = mapView.getOverlays(); + for (int i = overlays.size() - 1; i >= 0; i--) { + org.osmdroid.views.overlay.Overlay ov = overlays.get(i); + if (ov instanceof org.osmdroid.views.overlay.Marker) overlays.remove(i); + } + + org.osmdroid.views.overlay.Marker marker = new org.osmdroid.views.overlay.Marker(mapView); + marker.setPosition(new GeoPoint(location.getLatitude(), location.getLongitude())); + marker.setIcon(getResources().getDrawable(R.drawable.ic_location1)); + mapView.getOverlays().add(marker); + mapView.invalidate(); + } + + private void startLocationUpdatesForNavigation() { + locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + if (locationManager == null) { + Toast.makeText(this, "LocationManager недоступен", Toast.LENGTH_SHORT).show(); + return; + } + if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + Toast.makeText(this, "Включите GPS для навигации", Toast.LENGTH_LONG).show(); + return; + } + + locationListener = new LocationListener() { + @Override + public void onLocationChanged(@NonNull Location location) { + updateCurrentPosition(location); + checkDeviation(location); + } + + @Override + public void onProviderEnabled(@NonNull String provider) { + } + + @Override + public void onProviderDisabled(@NonNull String provider) { + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + } + }; + + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED) { + locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 3, locationListener); + } + } + + private void stopNavigationLocationUpdates() { + if (locationManager != null && locationListener != null) { + try { + locationManager.removeUpdates(locationListener); + } catch (SecurityException e) { + Log.w(TAG, "removeUpdates failed: " + e.getMessage()); + } + } + locationListener = null; + locationManager = null; + } + + private void checkDeviation(Location currentLocation) { + if (!isNavigationActive || routePoints == null || routePoints.isEmpty() || currentLocation == null) + return; + + GeoPoint currentPoint = new GeoPoint(currentLocation.getLatitude(), currentLocation.getLongitude()); + + double minDistance = Double.MAX_VALUE; + int closestIndex = -1; + for (int i = 0; i < routePoints.size(); i++) { + double d = currentPoint.distanceToAsDouble(routePoints.get(i)); + if (d < minDistance) { + minDistance = d; + closestIndex = i; + } + } + + if (closestIndex > currentSegmentIndex + 1) currentSegmentIndex = closestIndex - 1; + + if (currentSegmentIndex < routePoints.size() - 1) { + GeoPoint a = routePoints.get(currentSegmentIndex); + GeoPoint b = routePoints.get(currentSegmentIndex + 1); + double deviation = calculateDistanceToSegment(currentPoint, a, b); + if (deviation > MAX_DEVIATION_METERS) handleDeviation(deviation); + else { + double toEnd = currentPoint.distanceToAsDouble(b); + if (toEnd < 10) { + currentSegmentIndex++; + if (currentSegmentIndex >= routePoints.size() - 1) navigationComplete(); + } + } + } + } + + private double calculateDistanceToSegment(GeoPoint point, GeoPoint start, GeoPoint end) { + double A = point.getLatitude() - start.getLatitude(); + double B = point.getLongitude() - start.getLongitude(); + double C = end.getLatitude() - start.getLatitude(); + double D = end.getLongitude() - start.getLongitude(); + + double dot = A * C + B * D; + double lenSq = C * C + D * D; + double param = (lenSq != 0) ? (dot / lenSq) : -1; + + double xx, yy; + if (param < 0) { + xx = start.getLatitude(); + yy = start.getLongitude(); + } else if (param > 1) { + xx = end.getLatitude(); + yy = end.getLongitude(); + } else { + xx = start.getLatitude() + param * C; + yy = start.getLongitude() + param * D; + } + + double dx = point.getLatitude() - xx; + double dy = point.getLongitude() - yy; + + return Math.sqrt(dx * dx + dy * dy) * 111319.9; + } + + private void handleDeviation(double deviation) { + Log.w(TAG, "Отклонение: " + deviation); + triggerDeviationAlert(deviation); + updateDeviationVisualization(deviation); + } + + private void triggerDeviationAlert(double deviation) { + try { + Uri notification = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION); + Ringtone r = RingtoneManager.getRingtone(getApplicationContext(), notification); + if (r != null) r.play(); + } catch (Exception e) { + Log.w(TAG, "Sound failed: " + e.getMessage()); + } + + Vibrator v = (Vibrator) getSystemService(VIBRATOR_SERVICE); + if (v != null) v.vibrate(500); + + Toast.makeText(this, "Вы отклонились на " + (int) deviation + " м", Toast.LENGTH_SHORT).show(); + } + + private void updateDeviationVisualization(double deviation) { + if (mapView == null) return; + List overlays = mapView.getOverlays(); + for (org.osmdroid.views.overlay.Overlay ov : overlays) { + if (ov instanceof org.osmdroid.views.overlay.Polyline) { + org.osmdroid.views.overlay.Polyline pl = (org.osmdroid.views.overlay.Polyline) ov; + if (deviation > MAX_DEVIATION_METERS * 1.5) pl.setColor(0x88FF0000); + else if (deviation > MAX_DEVIATION_METERS) pl.setColor(0x88FFFF00); + else pl.setColor(0x8800FF00); + } + } + mapView.invalidate(); + } + + private void navigationComplete() { + isNavigationActive = false; + stopNavigationLocationUpdates(); + Toast.makeText(this, "Маршрут завершён!", Toast.LENGTH_LONG).show(); + // Сброс цвета маршрута + if (mapView != null) { + for (org.osmdroid.views.overlay.Overlay ov : mapView.getOverlays()) { + if (ov instanceof org.osmdroid.views.overlay.Polyline) { + ((org.osmdroid.views.overlay.Polyline) ov).setColor(0x880000FF); + } + } + mapView.invalidate(); + } + } + + + public void getSettings(View view) { + Intent intent = new Intent(MainActivity.this, SettingActivity.class); + startActivity(intent); + } + + // ========== SOS и отправка SMS (без изменения логики) ========== + public void SOS(View view) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) + != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(this, "Нет разрешения на локацию", Toast.LENGTH_SHORT).show(); + return; + } + fusedLocationClient.getLastLocation().addOnSuccessListener(this, location -> { + String message; + if (location == null) message = "SOS! Координаты не найдены."; + else + message = "SOS! Мои координаты: https://maps.google.com/?q=" + location.getLatitude() + "," + location.getLongitude(); + + List contacts = contactStorage.loadContacts(); + for (Contact c : contacts) sendSMS(message, c.getNumber()); + }); + } + + private void sendSMS(String message, String phoneNumber) { + Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:" + phoneNumber)); + intent.putExtra("sms_body", message); + if (intent.resolveActivity(getPackageManager()) != null) startActivity(intent); + else Toast.makeText(this, "Приложение для SMS не найдено", Toast.LENGTH_SHORT).show(); + } + + // ========== Lifecycle ========== + @Override + protected void onResume() { + super.onResume(); + if (mapView != null) mapView.onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + if (mapView != null) mapView.onPause(); + // Остановим активные запросы локации, чтобы не держать ресурсы + stopActiveLocationRequests(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + stopActiveLocationRequests(); + stopNavigationLocationUpdates(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/saferoute/ORSRequest.java b/app/src/main/java/com/example/saferoute/ORSRequest.java new file mode 100644 index 0000000..7e6d1ce --- /dev/null +++ b/app/src/main/java/com/example/saferoute/ORSRequest.java @@ -0,0 +1,14 @@ +package com.example.saferoute; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class ORSRequest { + @SerializedName("coordinates") + List coordinates; + + public ORSRequest(List coordinates) { + this.coordinates = coordinates; + } +} diff --git a/app/src/main/java/com/example/saferoute/ORSResponse.java b/app/src/main/java/com/example/saferoute/ORSResponse.java new file mode 100644 index 0000000..cbe009a --- /dev/null +++ b/app/src/main/java/com/example/saferoute/ORSResponse.java @@ -0,0 +1,20 @@ +package com.example.saferoute; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class ORSResponse { + @SerializedName("features") + public List features; + + public static class Feature { + @SerializedName("geometry") + public Geometry geometry; + } + + public static class Geometry { + @SerializedName("coordinates") + public List> coordinates; // [ [lon, lat], [lon, lat], ... ] + } +} diff --git a/app/src/main/java/com/example/saferoute/ORSService.java b/app/src/main/java/com/example/saferoute/ORSService.java new file mode 100644 index 0000000..e8e2d7c --- /dev/null +++ b/app/src/main/java/com/example/saferoute/ORSService.java @@ -0,0 +1,23 @@ +package com.example.saferoute; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.Query; + +public interface ORSService { + @GET("v2/directions/foot-hiking") + Call getFootRoute( + @Query("api_key") String apiKey, + @Query("start") String start, // Формат: "lon,lat" + @Query("end") String end // Формат: "lon,lat" + ); + + @POST("v2/directions/foot-walking/geojson") + Call getMultiPointRoute( + @Header("Authorization") String apiKey, + @Body ORSRequest body + ); +} diff --git a/app/src/main/java/com/example/saferoute/OSRMRouteResponse.java b/app/src/main/java/com/example/saferoute/OSRMRouteResponse.java new file mode 100644 index 0000000..ed1a0d3 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/OSRMRouteResponse.java @@ -0,0 +1,17 @@ +package com.example.saferoute; + +import java.util.List; + +public class OSRMRouteResponse { + public List routes; + + public static class Route { + public Geometry geometry; + public double distance; + public double duration; + } + + public static class Geometry { + public List> coordinates; // [longitude, latitude] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/saferoute/OSRMService.java b/app/src/main/java/com/example/saferoute/OSRMService.java new file mode 100644 index 0000000..e0fcdf4 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/OSRMService.java @@ -0,0 +1,19 @@ +package com.example.saferoute; + + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; +import retrofit2.http.Query; + +public interface OSRMService { + @GET("route/v1/foot/{coordinates}") + Call getRoute( + @Path("coordinates") String coordinates, + @Query("overview") String overview, + @Query("geometries") String geometries + ); + +} + +// http://router.project-osrm.org/route/v1/foot/64.543982,40.574575;64.529667,40.549826 \ No newline at end of file diff --git a/app/src/main/java/com/example/saferoute/SafeRouteService.java b/app/src/main/java/com/example/saferoute/SafeRouteService.java new file mode 100644 index 0000000..9f0ed70 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/SafeRouteService.java @@ -0,0 +1,134 @@ +package com.example.saferoute; + + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.os.IBinder; +import android.os.Looper; +import android.telephony.SmsManager; + +import androidx.annotation.Nullable; +import androidx.core.app.ActivityCompat; + +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationCallback; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationResult; + +public class SafeRouteService extends Service implements SensorEventListener { + private FusedLocationProviderClient locationClient; + private LocationRequest locationRequest; + private SensorManager sensorManager; + private Sensor accelerometer; + private Location lastLocation; + private long lastMoveTime; + private static final long STILL_THRESHOLD = 5 * 60 * 1000; + private static final float FALL_THRESHOLD = 30.0f; + private com.google.android.gms.location.LocationServices LocationServices; + + @SuppressLint("ForegroundServiceType") + @Override + public void onCreate() { + super.onCreate(); + initLocation(); + initSensors(); + startForeground(1, createNotification()); + } + + + private void initLocation() { + locationClient = LocationServices.getFusedLocationProviderClient(this); + locationRequest = LocationRequest.create() + .setInterval(10000) + .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return; + } + locationClient.requestLocationUpdates( + locationRequest, + locationCallback, + Looper.getMainLooper()); + } + private LocationCallback locationCallback = new LocationCallback() { + @Override + public void onLocationResult(LocationResult locationResult) { + Location current = locationResult.getLastLocation(); + if (lastLocation != null) { + float distance = current.distanceTo(lastLocation); + if (distance > 2.0) { + lastMoveTime = System.currentTimeMillis(); + } else { + checkStillness(); + } + } + lastLocation = current; + } + }; + private void checkStillness() { + if (System.currentTimeMillis() - lastMoveTime > STILL_THRESHOLD) { + sendAlertSMS("Пользователь долго не двигается!"); + stopSelf(); // Останавливаем после тревоги + } + } + private void initSensors() { + sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); + accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + sensorManager.registerListener(this, accelerometer, + SensorManager.SENSOR_DELAY_NORMAL); + } + @Override + public void onSensorChanged(SensorEvent event) { + float x = event.values[0]; + float y = event.values[1]; + float z = event.values[2]; + double acceleration = Math.sqrt(x * x + y * y + z * z); + if (acceleration > FALL_THRESHOLD) { + sendAlertSMS("Зафиксировано падение!"); + } + } + private void sendAlertSMS (String reason) { + String phone = "+79815540434"; + String message = "SOS! " + reason + " Мои координаты: " + + lastLocation.getLatitude() + ", " + lastLocation.getLongitude(); + SmsManager smsManager = SmsManager.getDefault(); + smsManager.sendTextMessage(phone, null, message, null, null); + } + private Notification createNotification () { + NotificationChannel channel = new NotificationChannel( + "safe_route", + "SafeRoute Active", + NotificationManager.IMPORTANCE_LOW); + getSystemService(NotificationManager.class).createNotificationChannel(channel); + return new Notification.Builder(this, "safe_route") + .setContentTitle("SafeRoute запущен") + .setContentText("Мы следим за вашей безопасностью") +// .setSmallIcon(R.drawable.ic_shield) + .build(); + } + @Nullable + @Override + public IBinder onBind(Intent intent) { + return null; + } + @Override + public void onAccuracyChanged(Sensor sensor, int i) { + + } +} diff --git a/app/src/main/java/com/example/saferoute/SettingActivity.java b/app/src/main/java/com/example/saferoute/SettingActivity.java new file mode 100644 index 0000000..8ca73bd --- /dev/null +++ b/app/src/main/java/com/example/saferoute/SettingActivity.java @@ -0,0 +1,32 @@ +package com.example.saferoute; + + +import android.content.Intent; +import android.os.Bundle; +import android.view.View; + +import androidx.appcompat.app.AppCompatActivity; + +import com.example.saferoute.contacts.ContactActivity; +import com.example.saferoute.places.PlaceActivity; + +public class SettingActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + } + + public void getBack(View view) { + Intent intent = new Intent(SettingActivity.this, MainActivity.class); + startActivity(intent); + } + public void contacts(View view){ + Intent intent = new Intent(SettingActivity.this, ContactActivity.class); + startActivity(intent); + } + public void places(View view){ + Intent intent = new Intent(SettingActivity.this, PlaceActivity.class); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/example/saferoute/contacts/Contact.java b/app/src/main/java/com/example/saferoute/contacts/Contact.java new file mode 100644 index 0000000..c009e59 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/contacts/Contact.java @@ -0,0 +1,38 @@ +package com.example.saferoute.contacts; + +import androidx.annotation.NonNull; + +public class Contact { +// private int id; + private String name; + private String number; + public Contact(String name, String number) { + this.name = name; + this.number = number; + } + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getNumber() { + return number; + } + public void setNumber(String number) { + this.number = number; + } +// public int getId() { +// return id; +// } +// public void setId(int id) { +// this.id = id; +// } + + @Override + public String toString() { + return name + " - " + number; + } +} + diff --git a/app/src/main/java/com/example/saferoute/contacts/ContactActivity.java b/app/src/main/java/com/example/saferoute/contacts/ContactActivity.java new file mode 100644 index 0000000..689e2e9 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/contacts/ContactActivity.java @@ -0,0 +1,159 @@ +package com.example.saferoute.contacts; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ListView; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import com.example.saferoute.R; +import com.example.saferoute.SettingActivity; + +import java.util.ArrayList; +import java.util.List; + +public class ContactActivity extends AppCompatActivity { + + private ListView listView; + private EditText inputName; + private EditText inputNumber; + private Button btnAdd; + + private ContactAdapter adapter; + private List contacts = new ArrayList<>(); + private ContactStorage contactStorage; + +// private SharedPreferences sharedPreferences; +// private Set names = new HashSet<>(); +// private String SAVED_NUMBERS = "SAVED_NUMBERS"; + + public void getBack2(View view){ + Intent intent = new Intent(ContactActivity.this, SettingActivity.class); + startActivity(intent); + } + + @SuppressLint("MissingInflatedId") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_contact); + + + // Инициализация View + listView = findViewById(R.id.list_contact); + inputName = findViewById(R.id.name1); + inputNumber = findViewById(R.id.number1); + btnAdd = findViewById(R.id.add_contact); + + contactStorage = new ContactStorage(this); + +// Загружаем контакты из памяти + contacts = contactStorage.loadContacts(); + + // Если контактов нет, добавим тестовые + if (contacts.isEmpty()) { +// addTestContacts(); + } + + // Настраиваем адаптер + adapter = new ContactAdapter(this, contacts); + listView.setAdapter(adapter); + + + btnAdd.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + addContact(); + } + }); + + + // Обработка нажатия на элемент списка + listView.setOnItemClickListener((parent, view, position, id) -> { + showDeleteDialog(position); + }); + } + + private void addContact() { + String name = inputName.getText().toString().trim(); + String phone = inputNumber.getText().toString().trim(); + + // Валидация полей + if (TextUtils.isEmpty(name)) { + inputName.setError("Введите имя"); + inputName.requestFocus(); + return; + } + + if (TextUtils.isEmpty(phone)) { + inputNumber.setError("Введите номер телефона"); + inputNumber.requestFocus(); + return; + } + + // Создаем новый контакт + Contact newContact = new Contact(name, phone); + + // Добавляем в список + contacts.add(newContact); + + // Сохраняем в SharedPreferences + contactStorage.saveContacts(contacts); + + // Обновляем ListView + adapter.notifyDataSetChanged(); + + // Очищаем поля ввода + inputName.setText(""); + inputNumber.setText(""); + + // Прокручиваем ListView к последнему элементу + listView.smoothScrollToPosition(contacts.size() - 1); + + Toast.makeText(this, "Контакт добавлен: " + name, Toast.LENGTH_SHORT).show(); + } + + private void showDeleteDialog(int position) { + Contact contact = contacts.get(position); + + new AlertDialog.Builder(this) + .setTitle("Удаление контакта") + .setMessage("Вы уверены, что хотите удалить контакт \"" + contact.getName() + "\"?") + .setPositiveButton("ДА", (dialog, which) -> { + deleteContact(position); + }) + .setNegativeButton("НЕТ", (dialog, which) -> { + dialog.dismiss(); + }) + .show(); + } + + private void deleteContact(int position) { + // Удаляем из списка + contacts.remove(position); + + // Сохраняем в памяти (SharedPreferences) + contactStorage.saveContacts(contacts); + + // Обновляем ListView + adapter.notifyDataSetChanged(); + + Toast.makeText(this, "Контакт удален", Toast.LENGTH_SHORT).show(); + } + + private void addTestContacts() { + contacts.add(new Contact("Мама", "+79115716167")); + contactStorage.saveContacts(contacts); + } + + + +} diff --git a/app/src/main/java/com/example/saferoute/contacts/ContactAdapter.java b/app/src/main/java/com/example/saferoute/contacts/ContactAdapter.java new file mode 100644 index 0000000..194f47f --- /dev/null +++ b/app/src/main/java/com/example/saferoute/contacts/ContactAdapter.java @@ -0,0 +1,41 @@ +package com.example.saferoute.contacts; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.example.saferoute.R; + +import java.util.List; + +public class ContactAdapter extends ArrayAdapter { + private final Context context; + private final List contacts; + + public ContactAdapter(@NonNull Context context, List contacts) { + super(context, R.layout.list_item_contact, contacts); + this.context = context; + this.contacts = contacts; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null){ + convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item_contact, parent, false); + } + + Contact contact = contacts.get(position); + + ((TextView) convertView.findViewById(R.id.name)).setText(contact.getName()); + ((TextView) convertView.findViewById(R.id.number)).setText(contact.getNumber()); + return convertView; + } + +} diff --git a/app/src/main/java/com/example/saferoute/contacts/ContactStorage.java b/app/src/main/java/com/example/saferoute/contacts/ContactStorage.java new file mode 100644 index 0000000..b5cce31 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/contacts/ContactStorage.java @@ -0,0 +1,35 @@ +package com.example.saferoute.contacts; + +import android.content.Context; +import android.content.SharedPreferences; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class ContactStorage { + private static final String PREF_NAME = "contacts_pref"; + private static final String KEY_CONTACTS = "contacts_list"; + private final SharedPreferences sharedPreferences; + private final Gson gson; + + public ContactStorage(Context context) { + sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + gson = new Gson(); + } + + public void saveContacts(List contacts) { + String json = gson.toJson(contacts); + sharedPreferences.edit().putString(KEY_CONTACTS, json).apply(); + } + + public List loadContacts() { + String json = sharedPreferences.getString(KEY_CONTACTS, ""); + if (json.isEmpty()) { + return new ArrayList<>(); + } + Type type = new TypeToken>() {}.getType(); + return gson.fromJson(json, type); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/saferoute/places/LocationPlaceActivity.java b/app/src/main/java/com/example/saferoute/places/LocationPlaceActivity.java new file mode 100644 index 0000000..d93bc25 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/LocationPlaceActivity.java @@ -0,0 +1,243 @@ +package com.example.saferoute.places; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Address; +import android.location.Geocoder; +import android.os.Bundle; +import android.text.InputType; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageButton; +import android.widget.Toast; + +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import com.example.saferoute.R; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationServices; + +import org.osmdroid.events.MapEventsReceiver; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.MapEventsOverlay; +import org.osmdroid.views.overlay.Marker; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +public class LocationPlaceActivity extends AppCompatActivity { + + private MapView map; + private Marker selectedMarker; + private GeoPoint selectedPoint; + private Button btnConfirm; + private EditText etSearch; + private ImageButton btnSearch; + + private boolean hasSelectedLocation = false; + private String pickedLocationName; + + private double lat, lon; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_location_picker); + + map = findViewById(R.id.map_picker); + btnConfirm = findViewById(R.id.btnConfirmLocation); + etSearch = findViewById(R.id.etSearchAddress); + btnSearch = findViewById(R.id.btnSearch); + + // Слушатель кнопки поиска + btnSearch.setOnClickListener(v -> { + String addressString = etSearch.getText().toString(); + if (!addressString.isEmpty()) { + searchAddress(addressString); + } + }); + + // Стандартная настройка карты + map.setMultiTouchControls(true); + map.getController().setZoom(15.0); + + + /* >> */ + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // Эта проверка на случай, если метод вызван без разрешений (например, onRequestPermissionsResult отказал) + return; + } + + // Попытка получить последнюю известную локацию (из кэша) + FusedLocationProviderClient fusedLocationClient = LocationServices.getFusedLocationProviderClient(this);; + fusedLocationClient.getLastLocation() + .addOnSuccessListener(this, location -> { + if (location != null) { + lat = location.getLatitude(); + lon = location.getLongitude(); + Log.e("LOCATION_TEST", "Найден getLastLocation: " + lat + " : " + lon); + // Центрируем где-нибудь (или на текущей позиции) +// map.getController().setCenter(new GeoPoint(64.5540, 40.5746)); + map.getController().setCenter(new GeoPoint(lat, lon)); + } + }) + .addOnFailureListener(this, e -> { + Log.e("LOCATION_TEST", "Ошибка getLastLocation: " + e.getMessage()); + // Если ошибка, тоже пытаемся запросить текущую + }); + + /* << */ + + + + map.getController().setCenter(new GeoPoint(lat, lon)); + + // 1. Создаем обработчик нажатий + MapEventsReceiver mReceive = new MapEventsReceiver() { + @Override + public boolean singleTapConfirmedHelper(GeoPoint p) { + // Вызывается при обычном тапе по карте + updateMarker(p); + return true; + } + + @Override + public boolean longPressHelper(GeoPoint p) { + // Вызывается при долгом нажатии (можно оставить пустым) + return false; + } + }; + + // 2. Добавляем слой событий на карту + MapEventsOverlay mapEventsOverlay = new MapEventsOverlay(mReceive); + map.getOverlays().add(0, mapEventsOverlay); // Добавляем самым первым слоем + + // Кнопка подтверждения + btnConfirm.setOnClickListener(v -> { + if (selectedPoint != null) { + showNameInputDialog(); + +// if (hasSelectedLocation) { +// // Возвращаем координаты в предыдущее Activity +// Intent resultIntent = new Intent(); +// resultIntent.putExtra("place", pickedLocationName); +// resultIntent.putExtra("lat", selectedPoint.getLatitude()); +// resultIntent.putExtra("lon", selectedPoint.getLongitude()); +// setResult(RESULT_OK, resultIntent); +// finish(); +// } + } + }); + } + + private void updateMarker(GeoPoint p) { + selectedPoint = p; + + // Если маркер уже был, просто двигаем его + if (selectedMarker == null) { + selectedMarker = new Marker(map); + selectedMarker.setTitle("Выбранное место"); + map.getOverlays().add(selectedMarker); + } + + selectedMarker.setPosition(p); + map.invalidate(); // Перерисовать карту + + // Показываем кнопку подтверждения + btnConfirm.setVisibility(View.VISIBLE); + btnConfirm.setText("Выбрать: " + String.format("%.4f", p.getLatitude()) + ", " + String.format("%.4f", p.getLongitude())); + } + private void searchAddress(String addressString) { + // Geocoder работает через интернет, поэтому в идеале его + // нужно вызывать в фоновом потоке, но для простоты используем текущий + Geocoder geocoder = new Geocoder(this, Locale.getDefault()); + try { + List
addresses = geocoder.getFromLocationName(addressString, 1); + + if (addresses != null && !addresses.isEmpty()) { + Address address = addresses.get(0); + GeoPoint foundPoint = new GeoPoint(address.getLatitude(), address.getLongitude()); + + // 1. Центрируем карту на найденном месте + map.getController().animateTo(foundPoint); + map.getController().setZoom(17.0); + + // 2. Ставим маркер + updateMarker(foundPoint); + + Toast.makeText(this, "Найдено: " + address.getAddressLine(0), Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Адрес не найден", Toast.LENGTH_SHORT).show(); + } + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(this, "Ошибка сети при поиске", Toast.LENGTH_SHORT).show(); + } + } + + private void showNameInputDialog() { + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Новая локация"); + builder.setMessage("Введите название для этого места (например: Дом, Работа, Дача):"); + + // Создаем EditText для ввода текста программно + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); // Текст с большой буквы + input.setHint("Название места"); + + // Добавляем отступы вокруг поля ввода, чтобы оно смотрелось аккуратно + FrameLayout container = new FrameLayout(this); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ); + // Конвертируем 16dp в пиксели для отступов + int marginInPx = (int) (16 * getResources().getDisplayMetrics().density); + params.leftMargin = marginInPx; + params.rightMargin = marginInPx; + input.setLayoutParams(params); + container.addView(input); + + builder.setView(container); + + // Кнопка "Сохранить" + builder.setPositiveButton("Сохранить", (dialog, which) -> { + String name = input.getText().toString().trim(); + if (name.isEmpty()) { + // Если ввели пустоту, даем дефолтное имя + this.pickedLocationName = "Моя локация"; + } else { + this.pickedLocationName = name; + } + // Возвращаем координаты в предыдущее Activity + Intent resultIntent = new Intent(); +// String.format("%.4f", selectedPoint.getLatitude()) + ", " + String.format("%.4f", p.getLongitude()) + resultIntent.putExtra("place", pickedLocationName); + resultIntent.putExtra("lat", selectedPoint.getLatitude()); + resultIntent.putExtra("lon", selectedPoint.getLongitude()); + setResult(RESULT_OK, resultIntent); + finish(); + + }); + + // Кнопка "Отмена" + builder.setNegativeButton("Отмена", (dialog, which) -> { + dialog.cancel(); + Toast.makeText(this, "Выбор названия отменен", Toast.LENGTH_SHORT).show(); + }); + + // Показываем окно + builder.show(); + } + +} diff --git a/app/src/main/java/com/example/saferoute/places/LocationRouteActivity.java b/app/src/main/java/com/example/saferoute/places/LocationRouteActivity.java new file mode 100644 index 0000000..1947d50 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/LocationRouteActivity.java @@ -0,0 +1,424 @@ +package com.example.saferoute.places; + +import static java.lang.Character.getType; + +import android.Manifest; +import android.app.AlertDialog; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.text.InputType; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.content.ContextCompat; + +import com.example.saferoute.ORSRequest; +import com.example.saferoute.ORSResponse; +import com.example.saferoute.ORSService; +import com.example.saferoute.R; +import com.google.android.gms.location.FusedLocationProviderClient; +import com.google.android.gms.location.LocationServices; +import com.google.gson.Gson; + +import org.osmdroid.events.MapEventsReceiver; +import org.osmdroid.util.GeoPoint; +import org.osmdroid.views.MapView; +import org.osmdroid.views.overlay.MapEventsOverlay; +import org.osmdroid.views.overlay.Marker; +import org.osmdroid.views.overlay.Overlay; +import org.osmdroid.views.overlay.Polyline; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class LocationRouteActivity extends AppCompatActivity { + + private MapView map; + private Marker selectedMarker; + + private boolean hasSelectedLocation = false; + private String pickedLocationName; + + private double lat, lon; + + + private List waypoints = new ArrayList<>(); // Список выбранных точек + private boolean isSelectedRoute = false; // Флаг: построен маршрут или нет + private boolean isSelectionMode = false; // Флаг: ставим мы точки или нет + private Button btnStartSelection, btnBuildRoute; + private Button btnConfirm; + + + private static final String ROUTES_FILE = "routes.json"; + private Gson gson = new Gson(); + + // Список точек окончательного проложенного маршрута (заполняется в drawRoute) + private List lastRoutePoints = new ArrayList<>(); + + // Executor для фоновых записей/чтения + private final java.util.concurrent.Executor executor = java.util.concurrent.Executors.newSingleThreadExecutor(); + + // Вложенные классы для сериализации маршрутов в routes.json + private static class SavedPoint { + double lat; + double lon; + SavedPoint() {} + SavedPoint(double lat, double lon) { this.lat = lat; this.lon = lon; } + } + + private static class SavedRoute { + long id; + String title; + List points; + long createdAt; + SavedRoute() {} + SavedRoute(String title, List points) { + this.id = System.currentTimeMillis(); + this.title = title; + this.points = points; + this.createdAt = System.currentTimeMillis(); + } + } + + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_location_pickers); + + map = findViewById(R.id.map_picker); + btnConfirm = findViewById(R.id.btnConfirmLocationRoute); + btnStartSelection = findViewById(R.id.btnStartSelection); + btnBuildRoute = findViewById(R.id.btnBuildRoute); + + +// Создаем приемник событий + MapEventsReceiver mReceive = new MapEventsReceiver() { + @Override + public boolean singleTapConfirmedHelper(GeoPoint p) { + if (isSelectionMode) { + Log.d("MAP_TAP", "Нажата точка: " + p.getLatitude() + ", " + p.getLongitude()); + addWaypoint(p); // Добавляем точку + return true; + } + return false; + } + + @Override + public boolean longPressHelper(GeoPoint p) { return false; } + }; + + // Добавляем его в самый конец списка оверлеев + MapEventsOverlay eventsOverlay = new MapEventsOverlay(mReceive); + map.getOverlays().add(eventsOverlay); + + + + ////////=============== + // Стандартная настройка карты + map.setMultiTouchControls(true); + map.getController().setZoom(15.0); + + + /* >> */ + if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // Эта проверка на случай, если метод вызван без разрешений (например, onRequestPermissionsResult отказал) + return; + } + + // Попытка получить последнюю известную локацию (из кэша) + FusedLocationProviderClient fusedLocationClient = LocationServices.getFusedLocationProviderClient(this); + fusedLocationClient.getLastLocation() + .addOnSuccessListener(this, location -> { + if (location != null) { + lat = location.getLatitude(); + lon = location.getLongitude(); + Log.e("LOCATION_TEST", "Найден getLastLocation: " + lat + " : " + lon); + // Центрируем где-нибудь (или на текущей позиции) +// map.getController().setCenter(new GeoPoint(64.5540, 40.5746)); + map.getController().setCenter(new GeoPoint(lat, lon)); + } + }) + .addOnFailureListener(this, e -> { + Log.e("LOCATION_TEST", "Ошибка getLastLocation: " + e.getMessage()); + // Если ошибка, тоже пытаемся запросить текущую + }); + + + btnStartSelection.setOnClickListener(v -> { + isSelectedRoute = false; + isSelectionMode = true; + waypoints.clear(); + + // Вместо clear() удаляем только маркеры и старые пути + // Проходим циклом с конца, чтобы безопасно удалять + Iterator it = map.getOverlays().iterator(); + while (it.hasNext()) { + Overlay overlay = it.next(); + if (overlay instanceof Marker || overlay instanceof Polyline) { + it.remove(); + } + } + + map.invalidate(); // Обновить карту + Toast.makeText(this, "Режим выбора: ставьте точки на карте", Toast.LENGTH_SHORT).show(); + }); + + + +// Когда пользователь нажал «Построить маршрут»: + btnBuildRoute.setOnClickListener(v -> { + if (waypoints.size() < 2) { + Toast.makeText(this, "Нужно минимум 2 точки", Toast.LENGTH_SHORT).show(); + return; + } + isSelectionMode = false; // Выключаем режим выбора + buildRoute(); + }); + + + // Кнопка подтверждения + btnConfirm.setOnClickListener(v -> { + if (isSelectedRoute) { + Toast.makeText(this, "ПУТЬ ЕСТЬ", Toast.LENGTH_SHORT).show(); + showNameInputDialog(); + }else + Toast.makeText(this, "ПУТи нет", Toast.LENGTH_SHORT).show(); + }); + } + + + + private void buildRoute() { + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl("https://api.openrouteservice.org/") // НОВЫЙ URL + .addConverterFactory(GsonConverterFactory.create()) + .build(); + + ORSService service = retrofit.create(ORSService.class); + + // Преобразуем List в формат для API [[lon, lat], [lon, lat], ...] + List coordsForApi = new ArrayList<>(); + for (GeoPoint p : waypoints) { + coordsForApi.add(new double[]{p.getLongitude(), p.getLatitude()}); + } + + ORSRequest requestBody = new ORSRequest(coordsForApi); + String apiKey = "eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjVhNDU5MGQxOTNjNzQxYTNhOTA4MDY5Nzg4ZWUxMDM1IiwiaCI6Im11cm11cjY0In0="; + + service.getMultiPointRoute(apiKey, requestBody).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + + + + // Получаем «сырые» координаты пути от ORS [[lon, lat], [lon, lat], ...] + List> rawCoords = response.body().features.get(0).geometry.coordinates; + + // Конвертируем их в List + List routePoints = new ArrayList<>(); + for (List coord : rawCoords) { + // Важно: ORS возвращает [Lon, Lat], а GeoPoint создается как (Lat, Lon) + double lon = coord.get(0); + double lat = coord.get(1); + routePoints.add(new GeoPoint(lat, lon)); + } + + // Передаем готовый список в ваш метод отрисовки + drawRoute(routePoints); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("API", "Ошибка: " + t.getMessage()); + } + }); + } + + private void addWaypoint(GeoPoint p) { + waypoints.add(p); + Log.d("WAYPOINTS", "Количество точек: " + waypoints.size()); + + Marker marker = new Marker(map); + marker.setPosition(p); + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); + marker.setTitle("Точка " + waypoints.size()); + + map.getOverlays().add(marker); + map.invalidate(); // Обязательно! Без этого маркер не появится сразу + } + + + + private void drawRoute(List points) { + if (points == null || points.isEmpty()) return; + + // Сохраняем, что это текущий показанный маршрут + lastRoutePoints = new ArrayList<>(points); + +// третья версия + // Удаляем старые Polyline-ы безопасно (итерация с конца) + for (int i = map.getOverlays().size() - 1; i >= 0; i--) { + org.osmdroid.views.overlay.Overlay ov = map.getOverlays().get(i); + if (ov instanceof org.osmdroid.views.overlay.Polyline) { + map.getOverlays().remove(i); + } + } + + org.osmdroid.views.overlay.Polyline polyline = new org.osmdroid.views.overlay.Polyline(); + polyline.setPoints(points); + polyline.setColor(0x880000FF); // Полупрозрачный синий + polyline.setWidth(10f); + + + map.getOverlays().add(polyline); + map.invalidate(); // Перерисовываем карту + + // Автоматически центрируем и масштабируем карту под маршрут + try { + map.zoomToBoundingBox(polyline.getBounds(), false); + } catch (Exception e) { + Log.w("DRAW_ROUTE", "Не удалось масштабировать bounding box: " + e.getMessage()); + } + + // Показываем кнопку подтверждения + btnConfirm.setVisibility(View.VISIBLE); + btnConfirm.setText("Сохранить путь?"); + isSelectedRoute = true; + } + + + private void showNameInputDialog() { + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Новый путь"); + builder.setMessage("Введите название для этого пути (например: Дом - Школа):"); + + // Создаем EditText для ввода текста программно + final EditText input = new EditText(this); + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES); // Текст с большой буквы + input.setHint("Название пути"); + + // Добавляем отступы вокруг поля ввода, чтобы оно смотрелось аккуратно + FrameLayout container = new FrameLayout(this); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT + ); + // Конвертируем 16dp в пиксели для отступов + int marginInPx = (int) (16 * getResources().getDisplayMetrics().density); + params.leftMargin = marginInPx; + params.rightMargin = marginInPx; + input.setLayoutParams(params); + container.addView(input); + + builder.setView(container); + + // Кнопка "Сохранить" + builder.setPositiveButton("Сохранить", (dialog, which) -> { + String name = input.getText().toString().trim(); + if (name.isEmpty()) { + name = "Мой путь"; + } + // Проверки: есть ли что сохранять + if (lastRoutePoints == null || lastRoutePoints.isEmpty()) { + Toast.makeText(this, "Нет маршрута для сохранения", Toast.LENGTH_SHORT).show(); + return; + } + + // Конвертируем GeoPoint -> SavedPoint + List pts = new ArrayList<>(); + for (GeoPoint gp : lastRoutePoints) { + pts.add(new SavedPoint(gp.getLatitude(), gp.getLongitude())); + } + + // Создаём объект маршрута + SavedRoute newRoute = new SavedRoute(name, pts); + + // Загружаем текущие маршруты, добавляем новый в начало и сохраняем в фоне + executor.execute(() -> { + List existing = loadSavedRoutesSync(); + // добавим в начало (последние сверху) + existing.add(0, newRoute); + // Сохраняем (сюда можно вызвать saveRoutesAsync, но мы в executor уже) + try (java.io.FileOutputStream fos = openFileOutput(ROUTES_FILE, MODE_PRIVATE)) { + String json = gson.toJson(existing); + fos.write(json.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } catch (Exception e) { + Log.e("RouteSave", "Ошибка сохранения маршрута: " + e.getMessage()); + } + + // После успешной записи вернёмся в UI-потоке + runOnUiThread(() -> { + // Вернём json нового маршрута (или просто сигнал) + Intent resultIntent = new Intent(); + resultIntent.putExtra("route_json", gson.toJson(newRoute)); + setResult(RESULT_OK, resultIntent); + Toast.makeText(LocationRouteActivity.this, "Маршрут сохранён", Toast.LENGTH_SHORT).show(); + finish(); + }); + }); + }); + + + // Кнопка "Отмена" + builder.setNegativeButton("Отмена", (dialog, which) -> { + dialog.cancel(); + Toast.makeText(this, "Выбор названия отменен", Toast.LENGTH_SHORT).show(); + }); + + // Показываем окно + builder.show(); + } + + // Синхронная загрузка существующих маршрутов из файла + private List loadSavedRoutesSync() { + List res = new ArrayList<>(); + try (java.io.FileInputStream fis = openFileInput(ROUTES_FILE)) { + byte[] bytes = new byte[fis.available()]; + fis.read(bytes); + String json = new String(bytes, java.nio.charset.StandardCharsets.UTF_8); + java.lang.reflect.Type t = new com.google.gson.reflect.TypeToken>(){}.getType(); + List loaded = gson.fromJson(json, t); + if (loaded != null) res = loaded; + } catch (Exception e) { + // файл может отсутствовать — возвращаем пустой список + Log.d("RouteSave", "routes file not found or error: " + e.getMessage()); + } + return res; + } + + // Асинхронная запись списка маршрутов в файл + private void saveRoutesAsync(List routes, Runnable onDone) { + executor.execute(() -> { + try (java.io.FileOutputStream fos = openFileOutput(ROUTES_FILE, MODE_PRIVATE)) { + String json = gson.toJson(routes); + fos.write(json.getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } catch (Exception e) { + Log.e("RouteSave", "saveRoutesAsync failed: " + e.getMessage()); + } + // вызвать onDone на UI-потоке, если нужно + if (onDone != null) { + runOnUiThread(onDone); + } + }); + } + +} diff --git a/app/src/main/java/com/example/saferoute/places/Place.java b/app/src/main/java/com/example/saferoute/places/Place.java new file mode 100644 index 0000000..ca26c3b --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/Place.java @@ -0,0 +1,37 @@ +package com.example.saferoute.places; + +public class Place { + int id; + String title; + String address; + double latitude; + double longitude; + + public Place(String title, double latitude, double longitude) { + this.title = title; + this.address = address; + this.latitude = latitude; + this.longitude = longitude; + } + + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } +} diff --git a/app/src/main/java/com/example/saferoute/places/PlaceActivity.java b/app/src/main/java/com/example/saferoute/places/PlaceActivity.java new file mode 100644 index 0000000..de54078 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/PlaceActivity.java @@ -0,0 +1,277 @@ +package com.example.saferoute.places; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.text.InputType; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ListView; +import android.widget.Toast; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; + +import com.example.saferoute.R; +import com.example.saferoute.SettingActivity; +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.List; + +public class PlaceActivity extends AppCompatActivity { + + private Button btnAddPlace, btnAddRoute; + + private PlaceStorage placesStorage; + private PlaceAdapter placesAdapter; + private ListView lvPlaces; + private List places= new ArrayList<>(); + + private RouteStorage routeStorage; +// private RoutesListAdapter routesAdapter; + private RouteAdapter routesAdapter; + private ListView lvRoutes; + private List routes = new ArrayList<>(); + + private Gson gson = new Gson(); + private static final String ROUTES_FILE = "routes.json"; + + String pickedLocationName = ""; // Переменная для хранения имени локации + private boolean hasSelectedLocation = false; + + public void getBack3(View view){ + Intent intent = new Intent(PlaceActivity.this, SettingActivity.class); + startActivity(intent); + } + + @SuppressLint("MissingInflatedId") + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_place); + + + lvPlaces = findViewById(R.id.lvPlaces); + lvRoutes = findViewById(R.id.lvRoutes); + btnAddPlace = findViewById(R.id.btnAddPlace); + btnAddRoute = findViewById(R.id.btnAddRoute); + + + placesStorage = new PlaceStorage(this); + routeStorage = new RouteStorage(this); + + places = placesStorage.loadPlaces(); + if (places.isEmpty()) { +// addTestContacts(); + } + + placesAdapter = new PlaceAdapter(this, places); + lvPlaces.setAdapter(placesAdapter); + +// routesAdapter = new RoutesListAdapter(this); + routesAdapter = new RouteAdapter(this, routes); + lvRoutes.setAdapter(routesAdapter); + + +// Обработчики действий адаптера +// routesAdapter.setOnActionListener(new RoutesListAdapter.OnActionListener() { +// @Override +// public void onUse(RouteModel route) { +// sendRouteToMain(route); +// } +// +// @Override +// public void onShow(RouteModel route) { +// // тут можно отправить Intent в Activity-карту показа маршрута, +// // или просто центрировать карту в Activity с картой, если она есть +// // пример: открыть Activity карты и передать маршрут (json) +// openMapWithRoute(route); +// } +// +// @Override +// public void onDelete(RouteModel route) { +// confirmAndDeleteRoute(route); +// } +// }); + +// Загрузка маршрутов из файла + routeStorage.loadRoutesAsync(list -> { + routes = list; + routesAdapter.setItems(routes); + }); + + +// Получение широты и долготы через карту + btnAddPlace.setOnClickListener(v -> { + // 2. Запускаем выбор локации + Intent intent = new Intent(this, LocationPlaceActivity.class); + locationPlaceLauncher.launch(intent); + }); + +// Получение пути через карту + btnAddRoute.setOnClickListener(v -> { + // 2. Запускаем выбор локации + Intent intent = new Intent(this, LocationRouteActivity.class); + locationRouteLauncher.launch(intent); + }); + + lvPlaces.setOnItemClickListener((parent, view, position, id) -> { + showDeletePlaceDialog(position); + }); + + lvRoutes.setOnItemClickListener((parent, view, position, id) -> { + showDeleteRouteDialog(position); + }); + } + + + +// если есть готовый маршрут (например, вернулся из Activity создания пути): + private void addRoute(Route route) { + if (route == null) return; + // добавить в список и сохранить + routes.add(0, route); // вставляем в начало + routesAdapter.setItems(routes); + routeStorage.saveRoutesAsync(routes); + } + + + private void deleteRoute(Route route) { + routes.remove(route); + routesAdapter.setItems(routes); + routeStorage.saveRoutesAsync(routes); + Toast.makeText(this, "Путь удален!", Toast.LENGTH_SHORT).show(); + + } + + + + private void showDeletePlaceDialog(int position) { + Place place = places.get(position); + + new AlertDialog.Builder(this) + .setTitle("Удаление места") + .setMessage("Вы уверены, что хотите удалить место? \"" + place.getTitle() + "\"?") + .setPositiveButton("ДА", (dialog, which) -> { + deletePlace(position); + }) + .setNegativeButton("НЕТ", (dialog, which) -> { + dialog.dismiss(); + }) + .show(); + } + + private void showDeleteRouteDialog(int position) { + Route route = routes.get(position); + + new AlertDialog.Builder(this) + .setTitle("Удаление пути") + .setMessage("Вы уверены, что хотите удалить путь? \"" + route.getTitle() + "\"?") + .setPositiveButton("ДА", (dialog, which) -> { +// deleteRoute(position); + deleteRoute(route); + }) + .setNegativeButton("НЕТ", (dialog, which) -> { + dialog.dismiss(); + }) + .show(); + } + + + private void addPlace(String place, double lat, double lon) { + + Place newPlace = new Place(place, lat, lon); + places.add(newPlace); + placesStorage.savePlaces(places); + + placesAdapter.notifyDataSetChanged(); + + lvPlaces.smoothScrollToPosition(places.size() - 1); + + } + + private void deletePlace(int position) { + // Удаляем из списка + places.remove(position); + + // Сохраняем в памяти (SharedPreferences) + placesStorage.savePlaces(places); + + // Обновляем ListView + placesAdapter.notifyDataSetChanged(); + + Toast.makeText(this, "Место удалено!", Toast.LENGTH_SHORT).show(); + } + + private void deleteRoute(int position) { + // Удаляем из списка + routes.remove(position); + + // Сохраняем в памяти (SharedPreferences) +// routeStorage.saveRoutes(places); + + // Обновляем ListView +// routeAdapter.notifyDataSetChanged(); + + Toast.makeText(this, "Путь удален!", Toast.LENGTH_SHORT).show(); + } + + private void addTestContacts() { + + places.add(new Place("Дом", 64.543982, 40.574575)); + places.add(new Place("Школа", 64.529667, 40.549826)); + places.add(new Place("Больница", 64.532115, 40.552135)); + placesStorage.savePlaces(places); + } + + + // 1. Регистрируем слушатель результата 1 кнопки + ActivityResultLauncher locationPlaceLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + String place = result.getData().getStringExtra("place"); + double lat = result.getData().getDoubleExtra("lat", 0); + double lon = result.getData().getDoubleExtra("lon", 0); + + // ЗАПИСЫВАЕМ: +// this.hasSelectedLocation = true; // Теперь мы знаем, что точка выбрана + // Запускаем показ всплывающего окошка для ввода названия! +// showNameInputDialog(); + addPlace(place, lat, lon); + + Toast.makeText(this, "Выбрано: " + lat + ", " + lon, Toast.LENGTH_SHORT).show(); + // Сохраните эти координаты или отправьте по СМС + } + } + ); + + // 1. Регистрируем слушатель результата 2 кнопки + ActivityResultLauncher locationRouteLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { +// здесь должно быть получение результата и добавление + + String routeJson = result.getData().getStringExtra("route_json"); + if (routeJson != null) { + // можно вставить в адаптер сразу: + Route route = new Gson().fromJson(routeJson, Route.class); + // добавить в локальный список и обновить ListView + addRoute(route); + Toast.makeText(this, "Путь добавлен: " + route.title, Toast.LENGTH_SHORT).show(); + } + } + } + ); + + +} + diff --git a/app/src/main/java/com/example/saferoute/places/PlaceAdapter.java b/app/src/main/java/com/example/saferoute/places/PlaceAdapter.java new file mode 100644 index 0000000..680a955 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/PlaceAdapter.java @@ -0,0 +1,43 @@ +package com.example.saferoute.places; + + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.example.saferoute.R; +import com.example.saferoute.contacts.Contact; + +import java.util.List; + +public class PlaceAdapter extends ArrayAdapter { + private final Context context; + private final List places; + + public PlaceAdapter(@NonNull Context context, List places) { + super(context, R.layout.list_item_place, places); + this.context = context; + this.places = places; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null){ + convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item_place, parent, false); + } + + Place place = places.get(position); + + ((TextView) convertView.findViewById(R.id.title)).setText(place.getTitle()); + ((TextView) convertView.findViewById(R.id.lat)).setText(String.format("Lat: %.4f", place.getLatitude())); + ((TextView) convertView.findViewById(R.id.lon)).setText(String.format("Lon: %.4f", place.getLongitude())); + return convertView; + } +} diff --git a/app/src/main/java/com/example/saferoute/places/PlaceStorage.java b/app/src/main/java/com/example/saferoute/places/PlaceStorage.java new file mode 100644 index 0000000..73db04f --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/PlaceStorage.java @@ -0,0 +1,38 @@ +package com.example.saferoute.places; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.example.saferoute.contacts.Contact; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class PlaceStorage { + private static final String PREF_NAME = "places_pref"; + private static final String KEY_PLACES = "places_list"; + private final SharedPreferences sharedPreferences; + private final Gson gson; + + public PlaceStorage(Context context) { + sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + gson = new Gson(); + } + + public void savePlaces(List places) { + String json = gson.toJson(places); + sharedPreferences.edit().putString(KEY_PLACES, json).apply(); + } + + public List loadPlaces() { + String json = sharedPreferences.getString(KEY_PLACES, ""); + if (json.isEmpty()) { + return new ArrayList<>(); + } + Type type = new TypeToken>() {}.getType(); + return gson.fromJson(json, type); + } +} diff --git a/app/src/main/java/com/example/saferoute/places/Point.java b/app/src/main/java/com/example/saferoute/places/Point.java new file mode 100644 index 0000000..6cdd455 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/Point.java @@ -0,0 +1,19 @@ +package com.example.saferoute.places; + +public class Point { + + public double lat; + public double lon; + + public Point() {} + + public Point(double lat, double lon) { this.lat = lat; this.lon = lon; } + + @Override + public String toString() { + + String s1 = String.format("%.4f", lat); + String s2 = String.format("%.4f", lon); + return "Lat=" + s1 + "; Lon=" + s2; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/saferoute/places/Route.java b/app/src/main/java/com/example/saferoute/places/Route.java new file mode 100644 index 0000000..7b54ad6 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/Route.java @@ -0,0 +1,39 @@ +package com.example.saferoute.places; + +import java.util.List; + +public class Route { + + public long id; // можно генерировать timestamp + public String title; + public List points; + public long createdAt; + + public Route() { + } + + public Route(String title, List points) { + this.id = System.currentTimeMillis(); + this.title = title; + this.points = points; + this.createdAt = System.currentTimeMillis(); + } + + public String getTitle() { + return title; + } + + public List getPoints() { + return points; + } + + public String[] getInfo() { + if (points != null && points.size() != 0) { + String s1 = points.get(0).toString(); + String s2 = points.get(points.size() - 1).toString(); + + return new String[]{s1, s2}; + } else return new String[]{"нет информации", "нет информации"}; + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/saferoute/places/RouteAdapter.java b/app/src/main/java/com/example/saferoute/places/RouteAdapter.java new file mode 100644 index 0000000..8dda593 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/RouteAdapter.java @@ -0,0 +1,52 @@ +package com.example.saferoute.places; + + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.example.saferoute.R; + +import java.util.List; + +public class RouteAdapter extends ArrayAdapter { + private final Context context; + private final List routes; + + public RouteAdapter(@NonNull Context context, List routes) { + super(context, R.layout.list_item_route, routes); + this.context = context; + this.routes = routes; + } + + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + if (convertView == null){ + convertView = LayoutInflater.from(getContext()).inflate(R.layout.list_item_route, parent, false); + } + + Route route = routes.get(position); + + ((TextView) convertView.findViewById(R.id.title)).setText(route.getTitle()); + ((TextView) convertView.findViewById(R.id.info1)).setText(route.getInfo()[0]); + ((TextView) convertView.findViewById(R.id.info2)).setText(route.getInfo()[1]); + + return convertView; + } + + public void setItems(List list) { + routes.clear(); + if (list != null) routes.addAll(list); + notifyDataSetChanged(); + } + + @Override + public int getCount() { return routes.size(); } +} diff --git a/app/src/main/java/com/example/saferoute/places/RouteStorage.java b/app/src/main/java/com/example/saferoute/places/RouteStorage.java new file mode 100644 index 0000000..3b242e4 --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/RouteStorage.java @@ -0,0 +1,73 @@ +package com.example.saferoute.places; + +import android.content.Context; +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class RouteStorage { + private static final String FILE_NAME = "routes.json"; + private final Context ctx; + private final Gson gson = new Gson(); + private final Executor executor = Executors.newSingleThreadExecutor(); + + public RouteStorage(Context ctx) { + this.ctx = ctx.getApplicationContext(); + } + + // Асинхронно сохраняет список (без callback) + public void saveRoutesAsync(List routes) { + executor.execute(() -> saveRoutesSync(routes)); + } + + // Синхронная запись (внутри executor вызывается) + public void saveRoutesSync(List routes) { + try (FileOutputStream fos = ctx.openFileOutput(FILE_NAME, Context.MODE_PRIVATE)) { + String json = gson.toJson(routes); + fos.write(json.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + Log.e("RouteStorage", "saveRoutesSync failed", e); + } + } + + // Асинхронно загружает и вызывает callback на UI thread + public void loadRoutesAsync(LoadCallback callback) { + executor.execute(() -> { + List list = loadRoutesSync(); + // вызываем callback в UI-потоке + android.os.Handler mainHandler = new android.os.Handler(ctx.getMainLooper()); + mainHandler.post(() -> callback.onLoaded(list)); + }); + } + + // Синхронная загрузка + public List loadRoutesSync() { + List res = new ArrayList<>(); + try (FileInputStream fis = ctx.openFileInput(FILE_NAME)) { + byte[] bytes = new byte[fis.available()]; + fis.read(bytes); + String json = new String(bytes, StandardCharsets.UTF_8); + Type t = new TypeToken>(){}.getType(); + List loaded = gson.fromJson(json, t); + if (loaded != null) res = loaded; + } catch (Exception e) { + // файл может не существовать — возвращаем пустой список + Log.d("RouteStorage", "loadRoutesSync: file not found or error, returning empty list"); + } + return res; + } + + public interface LoadCallback { + void onLoaded(List routes); + } +} diff --git a/app/src/main/java/com/example/saferoute/places/RoutesListAdapter.java b/app/src/main/java/com/example/saferoute/places/RoutesListAdapter.java new file mode 100644 index 0000000..940ad7d --- /dev/null +++ b/app/src/main/java/com/example/saferoute/places/RoutesListAdapter.java @@ -0,0 +1,95 @@ +package com.example.saferoute.places; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageButton; +import android.widget.TextView; + +import com.example.saferoute.R; + +import java.util.ArrayList; +import java.util.List; + +public class RoutesListAdapter extends BaseAdapter { + private final Context ctx; + private final List items = new ArrayList<>(); + private final LayoutInflater inflater; + private OnActionListener listener; + + public interface OnActionListener { +// void onUse(Route route); // нажали "Использовать" (отправить в MainActivity) +// void onShow(Route route); // нажали "Показать" (центровать/показать на карте) + void onDelete(Route route); // удалить маршрут + } + + public RoutesListAdapter(Context ctx) { + this.ctx = ctx; + this.inflater = LayoutInflater.from(ctx); + } + + public void setOnActionListener(OnActionListener l) { this.listener = l; } + + public void setItems(List list) { + items.clear(); + if (list != null) items.addAll(list); + notifyDataSetChanged(); + } + + public List getItems() { return items; } + + @Override + public int getCount() { return items.size(); } + + @Override + public Object getItem(int position) { return items.get(position); } + + @Override + public long getItemId(int position) { return items.get(position).id; } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + VH vh; + if (convertView == null) { + convertView = inflater.inflate(R.layout.list_item_route, parent, false); + vh = new VH(convertView); + convertView.setTag(vh); + } else vh = (VH) convertView.getTag(); + + final Route r = items.get(position); + vh.tvName.setText(r.title != null ? r.title : ("Маршрут " + r.id)); + int pointsCount = r.points != null ? r.points.size() : 0; +// vh.tvInfo.setText(pointsCount + " точек"); +// vh.tvInfo.setText(pointsCount + " точек • " + android.text.format.DateFormat.format("dd.MM.yy HH:mm", r.createdAt)); + +// vh.btnUse.setOnClickListener(v -> { +// if (listener != null) listener.onUse(r); +// }); +// +// vh.btnShow.setOnClickListener(v -> { +// if (listener != null) listener.onShow(r); +// }); + + // Длинный клик — удаление + convertView.setOnLongClickListener(v -> { + if (listener != null) listener.onDelete(r); + return true; + }); + + return convertView; + } + + static class VH { + TextView tvName, tvInfo1, tvInfo2; +// ImageButton btnUse, btnShow; + VH(View v) { + tvName = v.findViewById(R.id.title); + tvInfo1 = v.findViewById(R.id.info1); + tvInfo2 = v.findViewById(R.id.info2); +// btnUse = v.findViewById(R.id.btnUse); +// btnShow = v.findViewById(R.id.btnShow); + } + } +} diff --git a/app/src/main/res/drawable/angle_left.xml b/app/src/main/res/drawable/angle_left.xml new file mode 100644 index 0000000..71c1b4e --- /dev/null +++ b/app/src/main/res/drawable/angle_left.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..ca3826a --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_location.png b/app/src/main/res/drawable/ic_location.png new file mode 100644 index 0000000..154c05e Binary files /dev/null and b/app/src/main/res/drawable/ic_location.png differ diff --git a/app/src/main/res/drawable/ic_location1.png b/app/src/main/res/drawable/ic_location1.png new file mode 100644 index 0000000..90bd6db Binary files /dev/null and b/app/src/main/res/drawable/ic_location1.png differ diff --git a/app/src/main/res/drawable/icon.png b/app/src/main/res/drawable/icon.png new file mode 100644 index 0000000..a2193e4 Binary files /dev/null and b/app/src/main/res/drawable/icon.png differ diff --git a/app/src/main/res/drawable/icon2.png b/app/src/main/res/drawable/icon2.png new file mode 100644 index 0000000..b9cb2a0 Binary files /dev/null and b/app/src/main/res/drawable/icon2.png differ diff --git a/app/src/main/res/drawable/settings.xml b/app/src/main/res/drawable/settings.xml new file mode 100644 index 0000000..4af8a43 --- /dev/null +++ b/app/src/main/res/drawable/settings.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/activity_contact.xml b/app/src/main/res/layout/activity_contact.xml new file mode 100644 index 0000000..44e8ae6 --- /dev/null +++ b/app/src/main/res/layout/activity_contact.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + +